Problem
这是 #506 的完整方案重提。#506 的目标是让团队成员共同维护 Skill;关闭时维护者指出,必须先明确 Skill Maintainer 关系、权限边界、发布目标选择、审核/生命周期授权、审计归因以及 Web/API/CLI 兼容性。本 Issue 补齐这些约束,并将其定义为可分阶段实现、可灰度、可回滚、可验收的生产方案。
本文基于 main@d2403bb5911953b8f53e62c3f0a9edc291363944:
目标:在不破坏 Owner 隔离和旧客户端行为的前提下,让 Owner 将 TEAM namespace 中的 Skill 显式授权给其他成员维护版本;整个授权、发布、审核、读取、撤权、Owner 失效恢复、客户端同步和运维链路必须可审计且并发安全。
Proposed Solution
1. 核心模型与不可变约束
采用 单一 Owner + Skill 级 Maintainer + 显式 skillId 目标 :
skill.owner_id 仍是唯一所有者,不改为多人 Owner,也不改变 (namespace_id, slug, owner_id) 唯一约束。
Maintainer 是 Skill 级授权,不扩展 NamespaceRole,也不把 Skill 改成 namespace 共有资源。
首期只在 TEAM namespace 开启;TEAM 下 PUBLIC、NAMESPACE_ONLY、PRIVATE 均可授权,但 Maintainer 发布时 visibility 由目标 Skill 决定,不能借上传改变 visibility。Builtin/GLOBAL Skill 不接受 Maintainer;promotion 产生的 GLOBAL 副本不继承源 Skill 的 Maintainer。
所有协作 mutation 必须显式携带 targetSkillId;禁止仅凭 namespace/slug、Owner 名称或“唯一匹配”猜目标。
服务内部必须分离 actorUserId、targetSkillId、ownerId:Owner 从目标 Skill 读取,客户端不能提交/伪造。skill_version.created_by 是制品作者,review_task.submitted_by/reviewed_by 是提审/审核者,skill_version.published_by 是触发最终发布的人或系统,skill.updated_by 只表示最后修改 Skill 的 actor,四者不得互相代填。
Namespace ADMIN/OWNER 保留现有治理能力,但不会因 namespace 角色自动获得“向他人 Skill 上传任意新内容”的权限。本方案不给 SUPER_ADMIN 新增 target content write 或 grant 能力;首期仅保留审计化 Owner recovery,以及现有平台治理入口。未来如需内容 break-glass,应单独设计入口和二次授权。
Maintainer 授权本身不产生审核批准权;用户同时具有 Maintainer 和 Reviewer/Admin 身份时,能力取并集,但不能审核自己创建或提交的版本。
Maintainer 数量设可配置上限(建议默认 50/Skill);首期不支持授权过期时间和自定义细粒度角色。
2. 权限矩阵(首期冻结)
能力
Skill Owner
Skill Maintainer
Namespace ADMIN/OWNER
平台审核/治理角色
查看/下载 PRIVATE、草稿、扫描结果和本人可维护版本
✅
✅
保留现有读取规则
保留现有规则
查看 hidden/quarantined artifact
仅按现有安全规则
❌;只显示受限状态/原因
按现有治理规则
按现有治理规则
通过显式 skillId 上传新版本
✅
✅
仅当同时为 Owner/Maintainer
本方案不新增此能力
重试/删除未发布版本
可对符合规则的版本创建新 immutable attempt;物理删除按现有治理/保留策略
仅自己创建且非 PENDING_REVIEW 的版本;审核中版本按下一行的 submitted_by 规则撤回;操作落为 withdraw/new attempt,不原地覆盖制品
保留现有治理规则
保留现有治理规则
提交审核
任意可提交版本
仅 version.created_by == actor
保留现有提交规则
保留现有审核治理规则
撤回审核
任意 PENDING,但紧急撤回必填 reason
仅 review_task.submitted_by == actor
仅自己提交的任务
保留现有审核治理规则
PRIVATE 版本确认、rerelease
✅
仅自己产生的新版本;rerelease 归因真实 actor
保留现有治理规则
本方案不新增此能力
修改 visibility、archive/unarchive
✅/保留现有规则
❌
保留现有治理规则
保留现有规则
普通 tag/label、promotion
✅/保留现有规则
❌(首期明确不开放)
保留现有规则
保留现有规则
grant/revoke Maintainer
grant/revoke
只能 self-revoke
ADMIN/OWNER 不能 grant 任何人,只能带 reason 紧急 revoke
本方案不新增 grant;Owner recovery 后按 Owner 流程
hard delete、hide/yank
保留现有规则
❌
保留现有规则
保留现有规则
approve/reject review
仅现有 reviewer 权限且满足四眼原则
Maintainer 身份不授予
现有 reviewer 权限且满足四眼原则
现有 reviewer 权限且满足四眼原则
转移 Owner
可主动转移
❌
Namespace OWNER 仅做失效恢复;ADMIN 不可转移
GLOBAL 仅 SUPER_ADMIN 恢复
前端按钮只是 UX;所有入口必须在服务端通过同一个 SkillAccessPolicy 重新判定。建议能力至少拆成 canReadRestricted、canPublishVersion、canRetryOwnVersion、canWithdrawOwnVersion、canSubmitReview、canConfirmPrivate、canRerelease、canManageMaintainers、canChangeVisibility、canArchive、canDeleteSkill、canReview、canTransferOwnership,不再用单一 canManageLifecycle 代替全部权限。
3. 数据模型与授权生命周期
使用合并时的下一可用 Flyway 版本(当前 main 的下一版本为 V44)新增活动授权表:
CREATE TABLE skill_maintainer (
id BIGSERIAL PRIMARY KEY ,
skill_id BIGINT NOT NULL ,
namespace_id BIGINT NOT NULL ,
user_id VARCHAR (128 ) NOT NULL ,
granted_by VARCHAR (128 ) NOT NULL REFERENCES user_account(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ,
UNIQUE (skill_id, user_id),
FOREIGN KEY (skill_id, namespace_id)
REFERENCES skill(id, namespace_id) ON DELETE RESTRICT,
FOREIGN KEY (namespace_id, user_id)
REFERENCES namespace_member(namespace_id, user_id) ON DELETE RESTRICT
);
CREATE INDEX idx_skill_maintainer_user_skill
ON skill_maintainer(user_id, skill_id);
为复合 FK 给 skill(id, namespace_id) 增加对应唯一键。Owner 不写入此表;“用户必须是 active namespace member”“不能授权 Owner”“只能 TEAM namespace”“数量上限”等跨表规则在同一事务的领域服务中校验,FK/唯一约束作为最后防线。
其余 schema 采用多个 expand migration,不能把高风险索引和数据修复塞进同一个 V44:
skill.revision BIGINT NOT NULL DEFAULT 0,由 @Version 管理;任何内容、ACL、Owner、成员自动撤权 mutation 都递增。它同时作为 ETag 和 upload session 的 authorization generation,确保 revoke → regrant 后旧 staging 也不能复活。
skill_upload_session 至少包含 UUID PK、到 Skill/User/最终 Version 的 FK、idempotency_key/request_hash/local_package_fingerprint/expected_skill_revision、object prefix、STAGING/FINALIZING/FINALIZED/FAILED/EXPIRED、lease owner/expiry、TTL;Skill 删除使用 RESTRICT 并先清理/终止 session;唯一 (actor_user_id, skill_id, idempotency_key),并为 (status, expires_at) 建清理索引。
skill_version.bundle_storage_key 先 nullable;读取时若为 NULL 继续推导 legacy key,新写入必须填 UUID key,历史记录异步回填后再考虑收紧。published_by 对历史记录允许 NULL/UNKNOWN,只在能由 APPROVED ReviewTask 或 PRIVATE confirm 确定时回填,禁止伪造。
为保留审核证据且允许同一语义版本重试,将 skill_version 定义为 immutable attempt:新增 attempt_no NOT NULL DEFAULT 1、supersedes_version_id、artifact_fingerprint、review_requested_by/at;历史行为 attempt 1。将唯一约束从 (skill_id, version) 改为 (skill_id, version, attempt_no)。失败/拒绝/撤回后重试创建 attempt N+1,旧 row/files/ReviewTask 不原地覆盖。
为可恢复的异步扫描新增 scan_generation/scan_attempt_count/scan_deadline_at/scan_last_error;扫描投递经 outbox,结果必须携带 versionId + artifactFingerprint + scanGeneration,不能只凭版本号回写状态。
增加 SkillVersionStatus.WITHDRAWN 和 ReviewTaskStatus.WITHDRAWN。只有从未进入 scan/review 的 DRAFT 可直接物理删除;其他删除按 retention 软删除/撤回,最终清理由审计化保留策略执行。
skill_owner_transfer 至少包含 skill、from_owner_id/to_owner_id、expected_skill_revision、retain-old-owner 选项、PENDING/ACCEPTED/CANCELLED/EXPIRED、optimistic version、过期时间;partial unique 保证每 Skill 一个 PENDING,并有过期清理任务。
新增持久化 outbox 及唯一 event ID;第 7 节的 active-work partial unique index必须在后续独立 migration 创建。
授权生命周期必须满足:
grant/revoke/self-revoke 是幂等操作;重复 grant 不产生第二行或重复通知,重复 revoke 返回成功的最终状态。
grant/revoke/self-revoke 必须遵循第 6 节唯一的全局锁顺序(不能先锁 Skill 再回头锁 membership),并要求目标用户/namespace/Skill 均 active;候选人仅包含 active namespace member,排除 Owner 和已有 Maintainer。
mutation 提交前实时重查账号状态、namespace 状态、membership 和 ACL,不能只相信请求开始时缓存的 AuthContext。
移除 namespace member 时,在业务事务中记录 AUTO_REVOKED 审计/事件、递增受影响 Skill revision,再显式删除该 namespace 下全部 Maintainer 关系;FK 使用 RESTRICT,任何遗漏入口都会失败而不是静默绕过审计。重新加入不会自动恢复旧授权。
普通成员移除若目标仍拥有 Skill,返回 OWNED_SKILLS_REQUIRE_TRANSFER 并要求先完成 Owner transfer;安全事件下仅 Namespace OWNER 可带 reason 强制移除,随后必须走 owner recovery,不能静默改 Owner。
用户被禁用时立即使授权失效并自动撤权;重新启用后需要 Owner 重新授权。
账号合并流程必须迁移并去重授权:目标已是 Maintainer 时合并为一条,目标是 Owner 时删除冗余 ACL;同时处理 granted_by 和审计事件。若合并 Owner 后会违反 (namespace, slug, owner_id) 唯一约束,合并必须在写入前返回可操作冲突,不能留下 MERGED 账号名下的孤儿 Skill。
删除 Skill 前必须在同一业务事务显式审计并删除活动 ACL;FK RESTRICT 防止绕过。历史通过不可变审计记录保留。
Skill archive/frozen namespace 保留 ACL 但拒绝新写入;恢复 active 后 ACL 才重新有效。Namespace archived 拒绝所有维护 mutation。
所有授权变更使相关 capability/My Skills 缓存立即失效。
同时增加定时一致性巡检:孤儿 ACL、非成员 ACL、inactive 用户 ACL、Owner 自授权、GLOBAL/Builtin ACL、重复关系任一非零均告警,并提供只生成报告的 dry-run 修复工具。
4. 统一授权策略
新增集中式 SkillAccessPolicy(领域端口 + infra 查询实现),所有 Controller/AppService/DomainService 只查询 capability,不再分别拼接 Owner/Admin 判断。至少覆盖:
publish/validate/rerelease;
submit/withdraw/confirm/delete-version;
Skill/detail/version/file/download/private/draft/scan-result 读取;
VisibilityChecker、SkillQueryService、SkillLifecycleProjectionService、My Skills;
maintainer 管理、Owner 转移/恢复;
Web、/api/v1、CLI、sync、ClawHub compat、builtin、promotion 等全部入口。
对不可见的 PRIVATE/未发布 Skill,未授权 ID 访问使用统一 404 防止资源枚举;对调用者已能看见但无写权限的 Skill 返回 403。授权只决定“可做什么”,目标解析只接受 ID,二者不能混在一起。
5. API 契约
5.1 显式目标的版本 API
旧接口保持 Owner 自有 Skill 的兼容行为:
POST /api/web/skills/{namespace}/publish
POST /api/v1/skills/{namespace}/publish
POST /api/cli/v1/skills/{namespace}/publish
POST /api/cli/v1/skills/{namespace}/publish/validate
旧接口绝不更新 Maintainer 目标。如果 actor 没有自己的同 slug Skill、但命中了其维护的同 slug Skill,返回 409 TARGET_SKILL_REQUIRED,响应可列出调用者本就有权看到的 candidate skillId/ownerId 并提示升级,不能自动选中或创建影子 Skill;actor 已拥有同 slug Skill 时仍按旧契约更新自己的 Skill。
新增 canonical target 接口(/api/v1 与 /api/web 保持别名;CLI 单独保留认证契约):
POST /api/web/skills/id/{skillId}/versions/validate
POST /api/web/skills/id/{skillId}/versions
POST /api/v1/skills/id/{skillId}/versions/validate
POST /api/v1/skills/id/{skillId}/versions
POST /api/cli/v1/skills/id/{skillId}/versions/validate
POST /api/cli/v1/skills/id/{skillId}/versions
以及全部协作生命周期的 ID 路由:
DELETE /api/web/skills/id/{skillId}/versions/id/{versionId}
POST /api/web/skills/id/{skillId}/versions/id/{versionId}/withdraw-review
POST /api/web/skills/id/{skillId}/versions/id/{versionId}/rerelease
POST /api/web/skills/id/{skillId}/versions/id/{versionId}/submit-review
POST /api/web/skills/id/{skillId}/versions/id/{versionId}/confirm-publish
immutable attempt 后同一 semantic version 可有多条 row,因此所有生命周期 mutation 必须使用 versionId,不能再用 version string 猜 attempt。安装/公共下载按 version string 解析唯一 PUBLISHED attempt;授权用户的工作区详情默认展示 attemptNo 最大的当前 attempt,并同时返回完整历史。
DELETE .../versions/id/{versionId} 不是无审计硬删:从未扫描的 DRAFT 可物理删除;SCANNING/UPLOADED/SCAN_FAILED 等其余未发布 attempt 转为 WITHDRAWN/retention tombstone,证据和审计保留。对这些非审核中状态,Maintainer 仅能处理 created_by == actor 的 attempt,Skill Owner 可处理任意 attempt;Namespace ADMIN/OWNER 的紧急终止必须填写 reason。PENDING_REVIEW 不走上述 created-by 规则:必须调用 withdraw-review(或由 DELETE 返回 409 REVIEW_WITHDRAW_REQUIRED),并严格按第 7 节以 review_task.submitted_by 判权。响应必须返回实际执行的 WITHDRAWN|DELETED operation。
服务端加载目标后必须校验:目标存在且 active、TEAM namespace 一致且可写、actor active 且仍为 member、Owner/Maintainer capability、包内 name 解析出的 slug 等于目标 slug、requested visibility 未试图改变目标 visibility、版本状态与创建者约束。名称冲突判断比较 existing.id != target.id,不能再比较 existing.owner_id != actorUserId。
三个 precondition 使用不同名字和语义,不能混称 fingerprint:
localPackageFingerprint:服务端对本次规范化上传包计算的 SHA-256。validate 是可选的;若客户端先 validate,可在 publish 发送 X-Validated-Package-Fingerprint,服务端重算不一致返回 400 PACKAGE_FINGERPRINT_MISMATCH。
expectedSkillRevision:现有目标 Skill 的单调 revision。所有 target publish 必须发送标准 If-Match: "skill-{id}-r{revision}";Web/CLI 可从 detail/manifest/validate 获取,直接 publish 的客户端需先 GET。失败统一返回 HTTP 412 SKILL_PRECONDITION_FAILED,不用 409。
expectedRemoteVersion/expectedRemoteFingerprint:只属于 sync lockfile,用于检测 pull 后的远端内容漂移;作为 target validate/publish 的显式字段比较,漂移返回 409 REMOTE_CONTENT_CHANGED。
validate 响应至少返回 operation=UPDATE、skillId、ownerId、authorizedAs、resolvedSlug、resolvedVersion、localPackageFingerprint、skillRevision/ETag、当前 remote version/fingerprint。publish 响应固定包含 skillId/versionId/attemptNo/version/status/ownerId/createdBy/publishedBy/skillRevision。真实 publish 始终重新解析包和鉴权,不能把 validate 当授权凭证。
发布写接口使用独立 Idempotency-Key(X-Request-ID 只做 trace):key 作用域为 actor + route + target,并绑定 request hash。Session 状态为 STAGING -> FINALIZING(lease) -> FINALIZED 或 FAILED/EXPIRED:
相同 key/hash 的 FINALIZED 请求重放原 status/body/version ID;同 key 不同 hash 返回 409。
有效 FINALIZING lease 返回 409 + Retry-After,不能穿透执行;lease 过期后,reconciler 先按 session/version/outbox 判断 DB 是否已提交,已提交则补为 FINALIZED,否则原子取得新 lease 后重试/清理。
finalize 必须在创建 Version 的同一 DB 事务把 session 标为 FINALIZED,所以“提交成功、响应丢失”可安全重放;STAGING 崩溃由 TTL GC 回收。
现有幂等拦截器中 PROCESSING 请求仍可穿透的问题需一并修正,并补进程 crash/restart 测试。
稳定错误码至少包括:TARGET_SKILL_REQUIRED、SKILL_TARGET_NOT_FOUND、SKILL_MAINTAINER_REQUIRED、SKILL_TARGET_MISMATCH、SKILL_VERSION_CONFLICT、SKILL_ACTIVE_SUBMISSION_EXISTS、SKILL_PRECONDITION_FAILED(412)、REMOTE_CONTENT_CHANGED、MAINTAINER_NOT_NAMESPACE_MEMBER、MAINTAINER_LIMIT_REACHED、IDEMPOTENCY_KEY_REUSED。唯一约束/乐观锁异常必须映射为确定的 409,不能返回 500。
5.2 Maintainer 管理 API
GET /api/web/skills/id/{skillId}/maintainers
GET /api/web/skills/id/{skillId}/maintainer-candidates?page=&size=&keyword=
PUT /api/web/skills/id/{skillId}/maintainers/{userId}
DELETE /api/web/skills/id/{skillId}/maintainers/{userId}
DELETE /api/web/skills/id/{skillId}/maintainers/me
list 对 Owner、当前 Maintainer、Namespace ADMIN/OWNER、SUPER_ADMIN 可见;公共详情和搜索索引不暴露 Maintainer 身份。
candidates 只有可 grant 的 Skill Owner 可访问,必须数据库分页/搜索。
正常 grant/revoke 由 Skill Owner 执行;Maintainer 可 self-revoke;Namespace ADMIN/OWNER 只能紧急 revoke 且必须在 JSON body 填写 reason(不要放入可能被代理日志记录的 query string),不能 grant 任何人。本方案不提供 SUPER_ADMIN grant/content-write 捷径。
管理接口首期仅 portal session 可用,不通过 CLI/API token 暴露。版本读写仍要求现有 skill:read/skill:publish scope 与 ACL 同时满足,不新增一个可绕过 ACL 的 token scope。
5.3 Owner 主动转移与失效恢复
为防止 Owner 离职、禁用或移出 namespace 后无人能管理 ACL,补充:
POST /api/web/skills/id/{skillId}/owner-transfers
POST /api/web/skills/id/{skillId}/owner-transfers/{transferId}/accept
DELETE /api/web/skills/id/{skillId}/owner-transfers/{transferId}
POST /api/web/skills/id/{skillId}/owner-recovery
当前 active Skill Owner 可向另一位 active namespace member 创建有过期时间的 transfer request;禁止转给自己。请求固化 fromOwnerId/toOwnerId/expectedSkillRevision,目标用户必须显式 accept,Owner 可在 accept 前取消,不能单方面把责任转给别人。同一 Skill 同时至多一个 PENDING transfer。
当前 Owner inactive 或已不属于 namespace 时,TEAM namespace 仅 Namespace OWNER 可执行 recovery;Namespace ADMIN 不可。GLOBAL 仅 SUPER_ADMIN 可恢复。强制恢复必须填写 reason。
新 Owner 若已拥有相同 namespace/slug 的 Skill,返回 409 OWNER_TRANSFER_COORDINATE_CONFLICT,不得合并或覆盖。
accept/recovery 事务必须遵循第 6 节唯一的全局锁顺序,按序锁定涉及的 user、namespace、membership、Skill、Maintainer ACL 和 transfer request;不能先锁 Skill 再回头锁 membership。事务要求当前 owner_id == fromOwnerId 且 revision 未变化,并重新检查双方状态、过期时间与坐标冲突,再更新 owner_id/updated_by/revision;若新 Owner 原为 Maintainer则删除该 ACL。任何成功 transfer/recovery 原子取消该 Skill 其余 PENDING request,防止旧请求在恢复后接管。其他有效 Maintainer 保留并通知新 Owner复核。旧 Owner默认不自动降级为 Maintainer;仅普通 transfer 可由双方在请求中显式选择 retainPreviousOwnerAsMaintainer=true,recovery 永不保留失效 Owner。
Owner 失效不会后台静默改写 owner_id。在恢复前,有效 Maintainer 仍可执行内容维护,但 Owner 专属治理操作不可执行。
transfer/recovery 与 publish 通过同一 Skill 锁串行化;审计记录 before/after Owner、actor、reason、有效角色与 Maintainer 快照,并通知旧/新 Owner及当前 Maintainers。
6. 发布事务、并发与对象存储一致性
不能在对象上传期间长时间持有 Skill 数据库行锁。采用 UUID staging + 短事务 finalize + TTL GC/补偿 :
flowchart LR
A["Client: explicit skillId + package"] --> B["Initial validation and capability check"]
B --> C["Upload immutable objects to UUID staging"]
C --> D["Short DB transaction: acquire globally ordered locks"]
D --> E["Recheck user, membership, ACL, state, ETag"]
E --> F["Reserve/version write + audit + outbox"]
F --> G["Commit"]
G --> H["Scan/review/notification after commit"]
C --> I["Failure/expiry: GC and compensation retry"]
Loading
新增 upload session,至少记录 session_id、skill_id、actor_user_id、requested version、request hash/local package fingerprint、idempotency key、object prefix、创建时的 expected_skill_revision、lease/status、final version ID、过期时间。对象写入不可变 UUID 前缀;只有 finalize 后的 session 可被版本引用。skill_file.storage_key 与 skill_version.bundle_storage_key 保存实际 UUID key,GC 删除前必须反查确认没有任何 DB 引用。失败/过期对象由 TTL GC 删除,删除失败进入现有 storage compensation 重试并告警。
finalize 的短事务必须:
所有相关写事务只采用这一套全局锁顺序:user_account(ID 排序) -> namespace -> namespace_member(ID 排序) -> skill(skillId 排序) -> skill_maintainer -> skill_owner_transfer -> upload_session -> skill_version -> review_task。不需要的层级直接跳过,但不得反向加锁;批量成员移除、账号禁用和多 Skill 操作也必须按 ID 排序。检测到 serialization/deadlock 时仅做有界重试。
实时重查 actor 状态、namespace/membership/ACL、Skill status、package slug/visibility、If-Match,并要求当前 skill.revision == session.expected_skill_revision。因此 revoke、member removal、disable、transfer 或 regrant 后,撤权前创建的 session 永久失效,不能因重新授权复活。
创建新的 immutable attempt(绝不原地覆盖旧制品)、文件引用、真实 created_by、review submission intent、审计和持久化 outbox,并在同一事务将 session FINALIZED;提交后才触发 scan/通知。finalize 不更新 latest_version_id 或 canonical display name/summary;只有后续 PUBLISHED 状态转换事务可更新它们并写 published_by/updated_by。
DB 提交失败不留下可访问版本;staging 最终会被清理。对象存储失败不留下半写 DB 图。
并发语义:
同一 Skill 的内容 mutation、ACL mutation、Owner transfer 按上述锁顺序形成可解释的串行顺序;member removal/account disable 先使 live account/member 条件失效,再批量撤权并递增相关 Skill revision。
两个 Maintainer 同时上传同一 semantic version:一个成功创建新的 immutable attempt,另一个确定返回 409;只创建一个新 attempt,不留孤儿对象。
revoke/member removal/account disable 与 publish 并发:先提交者在序列中生效;安全判断以 finalize 的 live account/member/ACL + revision 为准,清理任务可以重试,但旧权限绝不能提交。
Owner transfer 与 publish 并发:始终写入同一 skill_id,Owner/actor 归因正确。
新 Skill 的旧 Owner 发布路径仍需按 #617 使用 coordinate lock/atomic conflict protocol;DB 唯一约束只作为最后防线。
7. 审核状态机与四眼原则
每个 Skill 同时最多一个 active work item,active 定义为 SCANNING、UPLOADED 或 PENDING_REVIEW。服务先在加锁事务内做业务校验,数据库 partial unique index 是最后防线:
CREATE UNIQUE INDEX uq_skill_one_active_submission
ON skill_version(skill_id)
WHERE status IN (' SCANNING' , ' UPLOADED' , ' PENDING_REVIEW' );
该索引不能与初始 expand schema 同批盲建。上线顺序固定为:只读生产预检 → 部署已修复 #617 且能阻止新重复的兼容 server(功能仍关闭)→ 对存量重复生成报告并由专项 remediation 显式撤回/修复、产生审计 → 再以独立 migration 建索引。当前代码创建 PRIVATE UPLOADED 时就写入 published_at/latest_version_id,所以这两个字段不能证明用户已经 confirm;迁移绝不能据此自动改成 PUBLISHED。兼容基线部署后,legacy UPLOADED 一律保留为未确认 attempt,将其错误的 published_at 归一为 NULL,latest_version_id 回退到最近的真实 PUBLISHED attempt(没有则 NULL),文件不删除;Owner 后续显式 confirm(缺少可信扫描结果时先重扫)或 withdraw。若同一 Skill 有多个 legacy active attempt,必须由专项报告逐条人工/Owner remediation 并写审计,不得自动猜选。大表优先使用 Flyway non-transactional CREATE UNIQUE INDEX CONCURRENTLY;若项目不允许,则必须走维护窗口并给出锁预算。预检非零时禁止执行建索引 migration,而不是仅关闭 feature flag。
状态规则:
所有 Maintainer target upload(包括 PRIVATE)都必须扫描;scanner 不可用时 target write fail closed。finalize 后进入 SCANNING。
扫描投递使用持久化 outbox 和幂等 job key。watchdog 扫描超过 scan_deadline_at 的 SCANNING:先以 CAS 取得恢复 lease,再按同一 scanGeneration 有界重投;超过最大次数或总时限后以 CAS 转为 SCAN_FAILED 并释放 active slot、告警和通知 Owner/作者。Owner/原 created_by 可显式 withdraw 自己有权处理的卡死 attempt;Namespace ADMIN/OWNER 仅可带 reason 紧急终止。
扫描结果处理器必须同时匹配 SCANNING 状态、artifactFingerprint 和当前 scanGeneration 才能转换状态;对已 WITHDRAWN/SCAN_FAILED、generation 已变化或被 supersede 的迟到结果只记录并丢弃,绝不能复活 attempt。watchdog、人工终止和结果回调使用同一 CAS/锁顺序。
PUBLIC/NAMESPACE_ONLY:SCANNING -> PENDING_REVIEW -> PUBLISHED|REJECTED;scan 失败进入 SCAN_FAILED。PRIVATE:SCANNING -> UPLOADED -> PUBLISHED(confirm),失败同样进入 SCAN_FAILED。
提审者不能在异步扫描中丢失:finalize/submit 在 skill_version.review_requested_by/at 持久化 submission intent;scan 成功创建 ReviewTask 时复制为 submitted_by。Owner 提交 Maintainer 制品时,created_by 与 submitted_by 必须保持不同的真实身份。
artifact attempt 一经 finalize 即不可变。SCAN_FAILED/REJECTED/WITHDRAWN 后可由 Owner 或原 created_by 以同一 semantic version 创建 attempt N+1;旧 attempt、文件和 ReviewTask 按 retention 保留。PUBLISHED/YANKED 版本禁止再用同一 semantic version 重试。
新上传遇到其他 active work item 返回 409 SKILL_ACTIVE_SUBMISSION_EXISTS,取消当前“自动撤回/覆盖别人待审版本”行为。同版本重试也只能在旧 attempt 已是 terminal 且调用者有权时创建新 attempt。
withdraw 使用 ReviewTask 的 @Version/CAS 将 PENDING -> WITHDRAWN,同时将对应 SkillVersion attempt 置为 WITHDRAWN,释放 active slot;不再删除 ReviewTask。Owner 撤回他人任务必须填写 reason,Maintainer/Namespace Admin/Owner 只能撤回自己 submitted_by 的任务(Skill Owner 的紧急权除外)。
PRIVATE UPLOADED 可由原作者或 Owner显式 withdraw 为 WITHDRAWN;confirm 后才更新 canonical metadata、latest_version_id、published_by。
latest_version_id 始终只指向 PUBLISHED attempt,不能指向 SCANNING/UPLOADED/PENDING_REVIEW/REJECTED/WITHDRAWN/YANKED。
approve/reject/withdraw 并发仅允许一个 CAS 成功,其他返回 409;latest_version_id 和 ReviewTask/Version 状态必须保持一致。
reviewer 不能等于 review_task.submitted_by 或 skill_version.created_by。即使用户同时为 Maintainer 和 Namespace ADMIN/Reviewer 也不能自审;首期不提供绕过四眼原则的 SUPER_ADMIN 内容发布捷径。
8. Web、查询与通知
“My Skills”改为数据库级分页的 owned UNION maintained,稳定按 updated_at DESC, id DESC 排序并去重;支持 Owned/Maintaining 筛选,显示 Owner 与关系 badge。
Skill detail/summary 返回 additive 的 relationships[] 和细粒度 capabilities;Maintainer 可看到 private/draft/scan/review 预览,但看不到 Owner 专属按钮。
Web Update 页进入时锁定并显示 skillId + namespace/slug + Owner,validate/publish 始终携带同一个 ID/ETag。权限在页面打开后被撤销时,服务端拒绝,UI 刷新 capability,不得回退到 legacy publish。
Maintainer 管理 UI 包含成员搜索分页、已有成员、self-revoke、上限/空态/错误态,并完成 i18n、a11y 和危险操作确认。
Search/public catalog 不因 Maintainer 关系改变;公共响应不泄露 ACL。
持久化 outbox/可重试消费者至少覆盖:
SKILL_MAINTAINER_GRANTED
SKILL_MAINTAINER_REVOKED
SKILL_MAINTAINER_AUTO_REVOKED
SKILL_OWNER_TRANSFERRED
SKILL_OWNER_RECOVERY_OVERRIDE
Maintainer 的上传、新 attempt、软删除/撤回、提审、PRIVATE confirm 和 rerelease
通知收件人按事件定义为 Owner + 当前有效 Maintainers(排除 actor、去重、排除 inactive);ACL 变更通知目标用户与 Owner;审核结果至少通知 created_by、submitted_by 和 Owner;break-glass 通知旧/新 Owner和当前 Maintainers。发布事件必须明确携带 authoredBy/submittedBy/reviewedBy/publishedBy/ownerId,不能继续假设 publisher 等于 Owner。订阅者仍只在真正 PUBLISHED 后收到新版本通知。通知只在事务提交后发送,可重试且不能影响授权正确性;同一事件必须有唯一 event ID 防重复。
9. 审计、安全与可观测性
授权、Owner 恢复及所有协作 mutation 写入不可变审计,至少包含:
skillId/namespaceId/versionId;
before/after ownerId;
真实 actorUserId、target user、grantor/revoker;
actor 当时有效的 Skill/namespace/platform roles;
request/correlation/idempotency ID、client/version、来源接口;
before/after 状态、reason、结果与时间。
不得记录 token、完整包内容或敏感文件。包内 metadata 不能改变 Owner/namespace/visibility;所有 IDOR、伪造 skillId、跨 namespace、非 member、inactive 用户、缺 token scope 场景必须 fail closed。
结构化日志/trace 覆盖 target resolution → policy → staging → finalize → review/outbox,关键指标包括:
grant/revoke/auto-revoke、Maintainer publish 的 success/403/409/5xx;
TARGET_SKILL_REQUIRED、shadow-skill 防护、active-work conflict;
Skill lock 等待/deadlock、唯一约束异常;
staging 数量/年龄/GC 失败、compensation backlog、outbox backlog/retry;
SCANNING age、watchdog retry/exhausted、迟到结果丢弃和 scan active-slot recovery;
My Skills/candidates p50/p95、sync push success/partial/failure、客户端和 manifest schema 版本。
上线前必须有 dashboard、告警和 runbook。以下至少触发停止放量:ACL 一致性巡检非零、出现影子 Skill、DB constraint/deadlock 或 orphan artifact 持续增长、owner legacy publish 成功率显著低于基线、目标发布 5xx 或延迟超过既定 SLO。具体绝对/相对阈值由运维基线记录在 runbook,不能在全量后补。
10. CLI 与 workspace sync(关联 #724 )
CLI publish 增加 --skill-id <id>(可保留 --target 作为 alias);维护现有 Skill 时为必需。dry-run 分别输出 localPackageFingerprint、skillRevision/ETag、remoteVersion/remoteFingerprint,不能用一个 fingerprint 字段混淆;JSON 字段 additive,错误有稳定非零 exit code。
新 CLI 遇到不支持 target API 的旧 server 必须 fail closed 并提示升级,不能退回 namespace/slug 发布。旧 CLI 对 Owner 自己 Skill 的 create/update 保持现状。
server contract PR 必须先提供并进入 OpenAPI:GET /api/cli/v1/namespaces/{namespace}/skills?page=&size=(installable manifest,仅返回调用者可读的 PUBLISHED;匿名只含 PUBLIC,认证读取 restricted 需 skill:read)和 GET /api/cli/v1/namespaces/{namespace}/manageable-skills?page=&size=(必须认证且有 skill:read + restricted ACL,返回用户可维护目标及 capability)。二者都返回分页、schemaVersion=2;manageable item 至少含 skillId/ownerId/namespace/slug/relationship/capabilities/skillRevision、headAttempt{version,attemptNo,status,fingerprint} 与 published{version,fingerprint}。sync 的 remoteVersion/remoteFingerprint 明确定义为 pull 时的 headAttempt,不与 published fingerprint 混用,也不能泄露无权维护的草稿。
管理 lockfile 持久化同一 schema。sync push 的 validate 和 publish 必须使用 lockfile 中同一 skillId + expectedSkillRevision + expectedRemoteVersion/Fingerprint;本地包另算 localPackageFingerprint。同 namespace 有多个 Owner 的同 slug Skill 时,只更新 lockfile 指向的 Skill;缺 ID/歧义/撤权一律非零退出且零写入,绝不能创建 actor 的影子 Skill。
明确定义 feat(cli): add namespace workspace sync #724 使用的 rejectExistingVersion=true:只要该 target 已存在相同 semantic version 的任一 attempt 就返回 409,不创建重试 attempt;普通手工发布若不设置该字段,才按第 7 节的 immutable-attempt 规则处理。publish response 必须包含上一节冻结的 status 等字段。
旧 manifest 升级若能通过已有安装元数据得到唯一稳定 ID则显式写回;否则要求用户选择/重新 pull。Skill Owner 转移不改变 skillId,下一次 pull 更新 ownerId。Skill 删除/撤权需给出明确 tombstone/forbidden 状态。
batch push 是“每个 Skill 原子”,不承诺跨 Skill 数据库事务。默认 preflight 全部目标,失败则不开始;执行期发生竞态时停止并报告已成功/未执行/失败项,使用幂等 key 可安全重试;若未来提供 --continue-on-error,必须明确是 opt-in。
发布顺序固定为:server contract PR 合并并部署 → 对真实 server 完成 CLI contract/E2E → feat(cli): add namespace workspace sync #724 rebase 后才开放 sync push。在此之前,feat(cli): add namespace workspace sync #724 即使先合并 pull/status/diff,也必须用独立 feature flag 禁用 push,不能依赖尚未进入 main 的接口假设。
11. Owner 之外的特殊入口
ClawHub compat 默认保持读取/旧契约,不新增隐式协作写入。
Builtin initializer 使用明确 system actor/target,不创建 Maintainer,不经过按 slug 猜协作目标。
rerelease 必须进入 target-aware 内部命令,不能像当前实现一样回到以 publisherId 查 Skill 的路径。
promotion/复制 GLOBAL Skill 不复制 ACL;首期 Maintainer 不能提交 promotion。
label/tag、hard delete、hide/yank、archive/unarchive 均必须显式测试 Maintainer 被拒绝,不能因复用“can manage”而意外放权。
12. 实施拆分
建议按可独立评审的 PR 拆分:
Compatibility baseline + expand schema :dual-read bundle、容忍新状态/attempt、revision/upload session/transfer/outbox 表和回填;功能关闭。
Policy + ACL lifecycle :集中式 SkillAccessPolicy、Maintainer API、成员移除/禁用/账号合并、Owner transfer/recovery、审计、锁顺序与 feature flag。
Target publish + state/concurrency/storage :显式 target validate/publish、immutable attempt、submission intent、rerelease/confirm/withdraw 重构、staging/finalize、幂等 lease、CAS、[Bug] Concurrent publishes race on skill coordinate and return 500 #617 的确定冲突映射。
Data remediation + invariant index :生产预检/修复工具与审计,随后独立 non-transactional/维护窗口 migration 创建 active-work unique index。
Read model + Web :restricted read、My Skills owned/maintained、capabilities、缓存失效、管理/发布 UI。
CLI + sync :--skill-id、两个 manifest endpoint、lockfile schema、feat(cli): add namespace workspace sync #724 server-first 适配和真实 E2E。
Rollout/operations :dashboard、alerts、巡检/GC/runbook、容量/查询计划验证、兼容基线/forward rollback 和全链路演练。
前置 PR 的接口需先落 OpenAPI/ADR;后续 PR 不得临时改变权限矩阵或目标解析语义。
Alternatives Considered
扩展 NamespaceRole :权限范围过大,会让成员维护 namespace 内所有 Skill,无法表达逐 Skill 委托;不采用。
多人 Owner / namespace 共有 Skill :会改变现有 Owner 隔离、唯一约束、查询和治理语义,迁移风险高;不采用。
删除 Owner 唯一维度 :会破坏既有同 slug/不同 Owner 数据;不采用。
按 namespace/slug/当前用户自动推断目标 :同 slug 可多条,actor 也可能同时拥有和维护不同 Skill,无法安全消歧;不采用。
让 Namespace ADMIN 自动发布任意内容 :治理角色不应默认成为供应链内容写入者;不采用。
上传期间持有数据库锁 :对象存储延迟会扩大锁时间和故障半径;采用 staging + 短 finalize 事务。
只改 Owner 校验 :无法处理读取、审核、通知、审计、客户端、撤权和并发,会产生影子 Skill/越权;不采用。
Impact
Migration / deployment / rollback
schema 是 expand-only,不给现有 Skill 回填 Maintainer,但必须把历史 skill_version.attempt_no 回填为 1、skill.revision 回填为 0,并按第 3 节 dual-read nullable bundle_storage_key/published_by;V43 → 全部新增迁移和全新安装都必须在 PostgreSQL 实测。
先发布“最低兼容回退基线”server:能 dual-read legacy/UUID bundle key、容忍并读取新 Version/Review 状态和 attempt 字段、识别新表但不开放协作写。新状态/UUID key 一旦开始写入,禁止回滚到该基线之前的旧二进制;此后的 rollback 是关写能力的 forward rollback,不是假装旧程序仍兼容。
部署顺序:生产只读预检 → expand schema → 所有实例升级到兼容基线且 feature flag 关闭 → 修复 [Bug] Concurrent publishes race on skill coordinate and return 500 #617 /存量 active 数据 → 独立建 partial unique index → Web/CLI → 指定 TEAM namespace canary → 观察 → 分批放量。混跑旧 server 时不得开启新写语义。
feature flag 至少拆为 assignment、Maintainer target write、sync push 和 namespace allowlist。回滚关闭 assignment/协作写/UI,但 canonical target 读取、Owner 管理新 attempt、dual-read、outbox/GC/补偿必须继续运行;保留表、ACL、审计和已发布版本,禁止 down migration/drop table。
必须演练“关闭 → Owner 仍能读取/管理新 attempt 并正常发布 → 重新开启后 ACL 仍一致”;已由 Maintainer 创建的版本在关闭期间仍由 Owner/现有治理角色管理。
My Skills/candidates 查询需验证索引和执行计划,并以生产规模数据定义 p95 预算;迁移和索引创建不得造成不可接受的长锁。
Release-blocking test matrix
Policy :Owner、Maintainer、普通 MEMBER、Namespace ADMIN、Namespace OWNER、Reviewer、SUPER_ADMIN、无关用户 × 每项 capability;TEAM/GLOBAL、active/frozen/archived、PUBLIC/NAMESPACE_ONLY/PRIVATE/hidden、各 version/review 状态。单独验收 submit 只认 created_by、普通 withdraw 只认 submitted_by、Skill Owner 紧急 withdraw 要求 reason。
ACL lifecycle :grant/regrant、revoke/re-revoke、self-revoke、非成员、inactive、Owner、数量上限、成员移除/Owner 移除阻断与强制恢复、禁用/启用、账号合并去重与 Owner 坐标冲突、Skill 删除、cache invalidation。
Publish attribution/immutability :Maintainer 发 v2 后 skill.id/owner_id 不变,只存在一条 aggregate;created_by/submitted_by/reviewed_by/published_by/skill.updated_by 分别等于真实制品作者、提审者、审核者、发布转换 actor、最后 Skill mutator。重试生成 attempt N+1,旧制品/审核证据不变;包伪造 Owner/visibility/slug 均失败。
Concurrency :同版本双 publish、不同版本同时提交、grant/revoke/regrant vs 旧 session、member removal/account disable vs finalize、bulk auto-revoke vs grant/recovery、Owner transfer/recovery/旧 transfer accept vs publish、approve/reject/withdraw、latest pointer/yank;使用真实 PostgreSQL barrier test 验证全局锁顺序/有界重试,结果确定且无 500。
Storage/idempotency/scan recovery :每个上传/DB/outbox/scan dispatch-result 边界及进程 crash/restart 注入失败;无半写、无永久孤儿、GC 不删引用对象、补偿可重试;相同/不同 hash、有效/过期 lease、提交成功响应丢失的行为正确。覆盖扫描消息丢失、重复/乱序/迟到结果、scanner 长时间不可用、watchdog 并发接管、最大重试转 SCAN_FAILED、人工 withdraw 与回调竞态,均不得复活 attempt 或永久占用 active slot。
Security :IDOR、跨 namespace、缺 scope、被撤权 stale page/token、inactive 账号、Maintainer 尝试 grant/转移/改 visibility/hard delete/hide/yank/archive/promotion/label/tag/self-review 均拒绝。
Queries/UI :owned + maintained 分页无重复且排序稳定;restricted preview;关系 badge/capabilities;候选搜索;撤权错误刷新;i18n/a11y/E2E。
Contracts :OpenAPI snapshot/generated SDK 无 drift;旧 Web/CLI + 新 server 保持传输契约和 Owner 目标解析兼容,自动撤回/原地替换/自审等故意收紧行为返回文档化 409/403;新 CLI + 旧 server fail closed;新 CLI + 新 server;feat(cli): add namespace workspace sync #724 server-first contract 和 sync push 真实 E2E。
Migration/ops :V43 升级、新装、历史 attempt/revision 回填、PRIVATE legacy UPLOADED 不误判发布且 published_at/latest_version_id 正确归一、bundle dual-read、两阶段 active index、最低兼容回退基线、蓝绿、forward rollback/re-enable;巡检 0 异常;dashboard/alerts/runbook;requestId 可从 API trace 追到 audit/version/outbox。
最终 release gate(Given/When/Then):
Owner 发布 v1,并将一个 active namespace MEMBER 授权为 Maintainer;
对方在 My Skills 看到 Maintaining,dry-run 明确显示 UPDATE 到原 skillId/ownerId;
Maintainer 发布 v2,Owner 不变、actor/audit/notification 正确、没有影子 Skill;
普通 MEMBER 不能读 private/draft 或写入;Maintainer 不能转授权、改 visibility、硬删、治理或自审;
Owner revoke 后,新的 publish/finalize 立即失败;重新授权后移除 namespace membership,ACL 自动清理且仍失败;
两个 Maintainer 同版本并发只有一个成功,另一个 409,无 500/孤儿对象/错误 latest pointer;
Owner 失效时 Maintainer 仍可内容维护,Namespace OWNER 可审计化恢复 Owner,普通 ADMIN 不可;
旧 Owner CLI 发布自己的 Skill 继续成功;feat(cli): add namespace workspace sync #724 sync 仅通过 lockfile skillId 更新指定 Skill;
feature flag 关闭和重新开启演练通过,ACL/版本/审计数据无损。
模拟扫描消息永久丢失后,watchdog 有界恢复或转 SCAN_FAILED 释放 active slot;随后新 attempt 可发布,迟到扫描结果不能复活旧 attempt。
Contract Or SDK Impact
新增 ID-based Web/v1/CLI API、Maintainer/Owner recovery API、稳定错误码、Idempotency-Key/ETag 语义;进入 OpenAPI、生成 SDK、operator docs 和 i18n。
DTO 字段做 additive 扩展:skillId、ownerId、relationships[]、细粒度 capabilities、operation/三个独立 precondition/revision;但新增 WITHDRAWN/attempt 语义要求先落可容忍未知状态和 UUID bundle key 的最低兼容 server/client 基线。
兼容矩阵:旧 client + 新 server(Owner 的传输协议/目标兼容,但自动撤回、原地覆盖和自审会按本文安全收紧);新 client + 新 server(完整能力);新 client + 旧 server(Maintainer 操作 fail closed,不 fallback);旧 client 无法协作维护但不得误写。
CLI 增加 --skill-id、JSON 字段和确定 exit codes;workspace manifest/lockfile 升 schema version并提供显式迁移策略。
本方案不改变公共安装坐标、不改变现有 Owner 唯一性、不把 Maintainer 身份暴露到公共搜索/下载协议。
Problem
这是 #506 的完整方案重提。#506 的目标是让团队成员共同维护 Skill;关闭时维护者指出,必须先明确 Skill Maintainer 关系、权限边界、发布目标选择、审核/生命周期授权、审计归因以及 Web/API/CLI 兼容性。本 Issue 补齐这些约束,并将其定义为可分阶段实现、可灰度、可回滚、可验收的生产方案。
本文基于
main@d2403bb5911953b8f53e62c3f0a9edc291363944:skill.owner_id是单一 Owner;数据库允许同一 namespace/slug 下存在不同 Owner 的 Skill(UNIQUE(namespace_id, slug, owner_id))。publisherId查找或创建 Skill(SkillPublishService#L399-L426)。仅放宽 Owner 校验会把 Maintainer 当成名称冲突,或在 Maintainer 名下创建“影子 Skill”。PENDING_REVIEW,并可能替换非发布版本(SkillPublishService#L432-L451);多人并发时会覆盖彼此工作。SkillGovernanceService#L298-L307、VisibilityChecker#L18-L37和SkillLifecycleProjectionService#L58-L69。owner_id(MySkillAppService#L81-L92)。通知也会忽略publisher != owner的发布事件(NotificationEventListener#L53-L66)。skillId,否则sync push仍会按 slug 猜目标。目标:在不破坏 Owner 隔离和旧客户端行为的前提下,让 Owner 将 TEAM namespace 中的 Skill 显式授权给其他成员维护版本;整个授权、发布、审核、读取、撤权、Owner 失效恢复、客户端同步和运维链路必须可审计且并发安全。
Proposed Solution
1. 核心模型与不可变约束
采用 单一 Owner + Skill 级 Maintainer + 显式
skillId目标:skill.owner_id仍是唯一所有者,不改为多人 Owner,也不改变(namespace_id, slug, owner_id)唯一约束。NamespaceRole,也不把 Skill 改成 namespace 共有资源。TEAMnamespace 开启;TEAM 下PUBLIC、NAMESPACE_ONLY、PRIVATE均可授权,但 Maintainer 发布时 visibility 由目标 Skill 决定,不能借上传改变 visibility。Builtin/GLOBAL Skill 不接受 Maintainer;promotion 产生的 GLOBAL 副本不继承源 Skill 的 Maintainer。targetSkillId;禁止仅凭 namespace/slug、Owner 名称或“唯一匹配”猜目标。actorUserId、targetSkillId、ownerId:Owner 从目标 Skill 读取,客户端不能提交/伪造。skill_version.created_by是制品作者,review_task.submitted_by/reviewed_by是提审/审核者,skill_version.published_by是触发最终发布的人或系统,skill.updated_by只表示最后修改 Skill 的 actor,四者不得互相代填。2. 权限矩阵(首期冻结)
skillId上传新版本submitted_by规则撤回;操作落为 withdraw/new attempt,不原地覆盖制品version.created_by == actorreview_task.submitted_by == actor前端按钮只是 UX;所有入口必须在服务端通过同一个
SkillAccessPolicy重新判定。建议能力至少拆成canReadRestricted、canPublishVersion、canRetryOwnVersion、canWithdrawOwnVersion、canSubmitReview、canConfirmPrivate、canRerelease、canManageMaintainers、canChangeVisibility、canArchive、canDeleteSkill、canReview、canTransferOwnership,不再用单一canManageLifecycle代替全部权限。3. 数据模型与授权生命周期
使用合并时的下一可用 Flyway 版本(当前
main的下一版本为 V44)新增活动授权表:为复合 FK 给
skill(id, namespace_id)增加对应唯一键。Owner 不写入此表;“用户必须是 active namespace member”“不能授权 Owner”“只能 TEAM namespace”“数量上限”等跨表规则在同一事务的领域服务中校验,FK/唯一约束作为最后防线。其余 schema 采用多个 expand migration,不能把高风险索引和数据修复塞进同一个 V44:
skill.revision BIGINT NOT NULL DEFAULT 0,由@Version管理;任何内容、ACL、Owner、成员自动撤权 mutation 都递增。它同时作为 ETag 和 upload session 的 authorization generation,确保 revoke → regrant 后旧 staging 也不能复活。skill_upload_session至少包含 UUID PK、到 Skill/User/最终 Version 的 FK、idempotency_key/request_hash/local_package_fingerprint/expected_skill_revision、object prefix、STAGING/FINALIZING/FINALIZED/FAILED/EXPIRED、lease owner/expiry、TTL;Skill 删除使用 RESTRICT 并先清理/终止 session;唯一(actor_user_id, skill_id, idempotency_key),并为(status, expires_at)建清理索引。skill_version.bundle_storage_key先 nullable;读取时若为 NULL 继续推导 legacy key,新写入必须填 UUID key,历史记录异步回填后再考虑收紧。published_by对历史记录允许 NULL/UNKNOWN,只在能由 APPROVED ReviewTask 或 PRIVATE confirm 确定时回填,禁止伪造。skill_version定义为 immutable attempt:新增attempt_no NOT NULL DEFAULT 1、supersedes_version_id、artifact_fingerprint、review_requested_by/at;历史行为 attempt 1。将唯一约束从(skill_id, version)改为(skill_id, version, attempt_no)。失败/拒绝/撤回后重试创建 attempt N+1,旧 row/files/ReviewTask 不原地覆盖。scan_generation/scan_attempt_count/scan_deadline_at/scan_last_error;扫描投递经 outbox,结果必须携带versionId + artifactFingerprint + scanGeneration,不能只凭版本号回写状态。SkillVersionStatus.WITHDRAWN和ReviewTaskStatus.WITHDRAWN。只有从未进入 scan/review 的 DRAFT 可直接物理删除;其他删除按 retention 软删除/撤回,最终清理由审计化保留策略执行。skill_owner_transfer至少包含 skill、from_owner_id/to_owner_id、expected_skill_revision、retain-old-owner 选项、PENDING/ACCEPTED/CANCELLED/EXPIRED、optimistic version、过期时间;partial unique 保证每 Skill 一个 PENDING,并有过期清理任务。授权生命周期必须满足:
AuthContext。AUTO_REVOKED审计/事件、递增受影响 Skill revision,再显式删除该 namespace 下全部 Maintainer 关系;FK 使用 RESTRICT,任何遗漏入口都会失败而不是静默绕过审计。重新加入不会自动恢复旧授权。OWNED_SKILLS_REQUIRE_TRANSFER并要求先完成 Owner transfer;安全事件下仅 Namespace OWNER 可带 reason 强制移除,随后必须走 owner recovery,不能静默改 Owner。granted_by和审计事件。若合并 Owner 后会违反(namespace, slug, owner_id)唯一约束,合并必须在写入前返回可操作冲突,不能留下 MERGED 账号名下的孤儿 Skill。同时增加定时一致性巡检:孤儿 ACL、非成员 ACL、inactive 用户 ACL、Owner 自授权、GLOBAL/Builtin ACL、重复关系任一非零均告警,并提供只生成报告的 dry-run 修复工具。
4. 统一授权策略
新增集中式
SkillAccessPolicy(领域端口 + infra 查询实现),所有 Controller/AppService/DomainService 只查询 capability,不再分别拼接 Owner/Admin 判断。至少覆盖:VisibilityChecker、SkillQueryService、SkillLifecycleProjectionService、My Skills;/api/v1、CLI、sync、ClawHub compat、builtin、promotion 等全部入口。对不可见的 PRIVATE/未发布 Skill,未授权 ID 访问使用统一 404 防止资源枚举;对调用者已能看见但无写权限的 Skill 返回 403。授权只决定“可做什么”,目标解析只接受 ID,二者不能混在一起。
5. API 契约
5.1 显式目标的版本 API
旧接口保持 Owner 自有 Skill 的兼容行为:
旧接口绝不更新 Maintainer 目标。如果 actor 没有自己的同 slug Skill、但命中了其维护的同 slug Skill,返回
409 TARGET_SKILL_REQUIRED,响应可列出调用者本就有权看到的 candidateskillId/ownerId并提示升级,不能自动选中或创建影子 Skill;actor 已拥有同 slug Skill 时仍按旧契约更新自己的 Skill。新增 canonical target 接口(
/api/v1与/api/web保持别名;CLI 单独保留认证契约):以及全部协作生命周期的 ID 路由:
immutable attempt 后同一 semantic version 可有多条 row,因此所有生命周期 mutation 必须使用
versionId,不能再用 version string 猜 attempt。安装/公共下载按 version string 解析唯一 PUBLISHED attempt;授权用户的工作区详情默认展示 attemptNo 最大的当前 attempt,并同时返回完整历史。DELETE .../versions/id/{versionId}不是无审计硬删:从未扫描的 DRAFT 可物理删除;SCANNING/UPLOADED/SCAN_FAILED等其余未发布 attempt 转为 WITHDRAWN/retention tombstone,证据和审计保留。对这些非审核中状态,Maintainer 仅能处理created_by == actor的 attempt,Skill Owner 可处理任意 attempt;Namespace ADMIN/OWNER 的紧急终止必须填写 reason。PENDING_REVIEW不走上述 created-by 规则:必须调用withdraw-review(或由 DELETE 返回409 REVIEW_WITHDRAW_REQUIRED),并严格按第 7 节以review_task.submitted_by判权。响应必须返回实际执行的WITHDRAWN|DELETEDoperation。服务端加载目标后必须校验:目标存在且 active、TEAM namespace 一致且可写、actor active 且仍为 member、Owner/Maintainer capability、包内 name 解析出的 slug 等于目标 slug、requested visibility 未试图改变目标 visibility、版本状态与创建者约束。名称冲突判断比较
existing.id != target.id,不能再比较existing.owner_id != actorUserId。三个 precondition 使用不同名字和语义,不能混称 fingerprint:
localPackageFingerprint:服务端对本次规范化上传包计算的 SHA-256。validate 是可选的;若客户端先 validate,可在 publish 发送X-Validated-Package-Fingerprint,服务端重算不一致返回 400PACKAGE_FINGERPRINT_MISMATCH。expectedSkillRevision:现有目标 Skill 的单调 revision。所有 target publish 必须发送标准If-Match: "skill-{id}-r{revision}";Web/CLI 可从 detail/manifest/validate 获取,直接 publish 的客户端需先 GET。失败统一返回 HTTP 412SKILL_PRECONDITION_FAILED,不用 409。expectedRemoteVersion/expectedRemoteFingerprint:只属于 sync lockfile,用于检测 pull 后的远端内容漂移;作为 target validate/publish 的显式字段比较,漂移返回 409REMOTE_CONTENT_CHANGED。validate 响应至少返回
operation=UPDATE、skillId、ownerId、authorizedAs、resolvedSlug、resolvedVersion、localPackageFingerprint、skillRevision/ETag、当前 remote version/fingerprint。publish 响应固定包含skillId/versionId/attemptNo/version/status/ownerId/createdBy/publishedBy/skillRevision。真实 publish 始终重新解析包和鉴权,不能把 validate 当授权凭证。发布写接口使用独立
Idempotency-Key(X-Request-ID只做 trace):key 作用域为 actor + route + target,并绑定 request hash。Session 状态为STAGING -> FINALIZING(lease) -> FINALIZED或FAILED/EXPIRED:Retry-After,不能穿透执行;lease 过期后,reconciler 先按 session/version/outbox 判断 DB 是否已提交,已提交则补为 FINALIZED,否则原子取得新 lease 后重试/清理。PROCESSING请求仍可穿透的问题需一并修正,并补进程 crash/restart 测试。稳定错误码至少包括:
TARGET_SKILL_REQUIRED、SKILL_TARGET_NOT_FOUND、SKILL_MAINTAINER_REQUIRED、SKILL_TARGET_MISMATCH、SKILL_VERSION_CONFLICT、SKILL_ACTIVE_SUBMISSION_EXISTS、SKILL_PRECONDITION_FAILED(412)、REMOTE_CONTENT_CHANGED、MAINTAINER_NOT_NAMESPACE_MEMBER、MAINTAINER_LIMIT_REACHED、IDEMPOTENCY_KEY_REUSED。唯一约束/乐观锁异常必须映射为确定的 409,不能返回 500。5.2 Maintainer 管理 API
skill:read/skill:publishscope 与 ACL 同时满足,不新增一个可绕过 ACL 的 token scope。5.3 Owner 主动转移与失效恢复
为防止 Owner 离职、禁用或移出 namespace 后无人能管理 ACL,补充:
fromOwnerId/toOwnerId/expectedSkillRevision,目标用户必须显式 accept,Owner 可在 accept 前取消,不能单方面把责任转给别人。同一 Skill 同时至多一个 PENDING transfer。409 OWNER_TRANSFER_COORDINATE_CONFLICT,不得合并或覆盖。owner_id == fromOwnerId且 revision 未变化,并重新检查双方状态、过期时间与坐标冲突,再更新owner_id/updated_by/revision;若新 Owner 原为 Maintainer则删除该 ACL。任何成功 transfer/recovery 原子取消该 Skill 其余 PENDING request,防止旧请求在恢复后接管。其他有效 Maintainer 保留并通知新 Owner复核。旧 Owner默认不自动降级为 Maintainer;仅普通 transfer 可由双方在请求中显式选择retainPreviousOwnerAsMaintainer=true,recovery 永不保留失效 Owner。owner_id。在恢复前,有效 Maintainer 仍可执行内容维护,但 Owner 专属治理操作不可执行。6. 发布事务、并发与对象存储一致性
不能在对象上传期间长时间持有 Skill 数据库行锁。采用 UUID staging + 短事务 finalize + TTL GC/补偿:
flowchart LR A["Client: explicit skillId + package"] --> B["Initial validation and capability check"] B --> C["Upload immutable objects to UUID staging"] C --> D["Short DB transaction: acquire globally ordered locks"] D --> E["Recheck user, membership, ACL, state, ETag"] E --> F["Reserve/version write + audit + outbox"] F --> G["Commit"] G --> H["Scan/review/notification after commit"] C --> I["Failure/expiry: GC and compensation retry"]新增 upload session,至少记录
session_id、skill_id、actor_user_id、requested version、request hash/local package fingerprint、idempotency key、object prefix、创建时的expected_skill_revision、lease/status、final version ID、过期时间。对象写入不可变 UUID 前缀;只有 finalize 后的 session 可被版本引用。skill_file.storage_key与skill_version.bundle_storage_key保存实际 UUID key,GC 删除前必须反查确认没有任何 DB 引用。失败/过期对象由 TTL GC 删除,删除失败进入现有 storage compensation 重试并告警。finalize 的短事务必须:
user_account(ID 排序) -> namespace -> namespace_member(ID 排序) -> skill(skillId 排序) -> skill_maintainer -> skill_owner_transfer -> upload_session -> skill_version -> review_task。不需要的层级直接跳过,但不得反向加锁;批量成员移除、账号禁用和多 Skill 操作也必须按 ID 排序。检测到 serialization/deadlock 时仅做有界重试。If-Match,并要求当前skill.revision == session.expected_skill_revision。因此 revoke、member removal、disable、transfer 或 regrant 后,撤权前创建的 session 永久失效,不能因重新授权复活。created_by、review submission intent、审计和持久化 outbox,并在同一事务将 session FINALIZED;提交后才触发 scan/通知。finalize 不更新latest_version_id或 canonical display name/summary;只有后续 PUBLISHED 状态转换事务可更新它们并写published_by/updated_by。并发语义:
skill_id,Owner/actor 归因正确。7. 审核状态机与四眼原则
每个 Skill 同时最多一个 active work item,active 定义为
SCANNING、UPLOADED或PENDING_REVIEW。服务先在加锁事务内做业务校验,数据库 partial unique index 是最后防线:该索引不能与初始 expand schema 同批盲建。上线顺序固定为:只读生产预检 → 部署已修复 #617 且能阻止新重复的兼容 server(功能仍关闭)→ 对存量重复生成报告并由专项 remediation 显式撤回/修复、产生审计 → 再以独立 migration 建索引。当前代码创建 PRIVATE
UPLOADED时就写入published_at/latest_version_id,所以这两个字段不能证明用户已经 confirm;迁移绝不能据此自动改成PUBLISHED。兼容基线部署后,legacyUPLOADED一律保留为未确认 attempt,将其错误的published_at归一为 NULL,latest_version_id回退到最近的真实PUBLISHEDattempt(没有则 NULL),文件不删除;Owner 后续显式 confirm(缺少可信扫描结果时先重扫)或 withdraw。若同一 Skill 有多个 legacy active attempt,必须由专项报告逐条人工/Owner remediation 并写审计,不得自动猜选。大表优先使用 Flyway non-transactionalCREATE UNIQUE INDEX CONCURRENTLY;若项目不允许,则必须走维护窗口并给出锁预算。预检非零时禁止执行建索引 migration,而不是仅关闭 feature flag。状态规则:
SCANNING。scan_deadline_at的SCANNING:先以 CAS 取得恢复 lease,再按同一scanGeneration有界重投;超过最大次数或总时限后以 CAS 转为SCAN_FAILED并释放 active slot、告警和通知 Owner/作者。Owner/原created_by可显式 withdraw 自己有权处理的卡死 attempt;Namespace ADMIN/OWNER 仅可带 reason 紧急终止。SCANNING状态、artifactFingerprint和当前scanGeneration才能转换状态;对已WITHDRAWN/SCAN_FAILED、generation 已变化或被 supersede 的迟到结果只记录并丢弃,绝不能复活 attempt。watchdog、人工终止和结果回调使用同一 CAS/锁顺序。SCANNING -> PENDING_REVIEW -> PUBLISHED|REJECTED;scan 失败进入SCAN_FAILED。PRIVATE:SCANNING -> UPLOADED -> PUBLISHED(confirm),失败同样进入SCAN_FAILED。skill_version.review_requested_by/at持久化 submission intent;scan 成功创建 ReviewTask 时复制为submitted_by。Owner 提交 Maintainer 制品时,created_by与submitted_by必须保持不同的真实身份。SCAN_FAILED/REJECTED/WITHDRAWN后可由 Owner 或原created_by以同一 semantic version 创建 attempt N+1;旧 attempt、文件和 ReviewTask 按 retention 保留。PUBLISHED/YANKED版本禁止再用同一 semantic version 重试。409 SKILL_ACTIVE_SUBMISSION_EXISTS,取消当前“自动撤回/覆盖别人待审版本”行为。同版本重试也只能在旧 attempt 已是 terminal 且调用者有权时创建新 attempt。@Version/CAS 将PENDING -> WITHDRAWN,同时将对应 SkillVersion attempt 置为WITHDRAWN,释放 active slot;不再删除 ReviewTask。Owner 撤回他人任务必须填写 reason,Maintainer/Namespace Admin/Owner 只能撤回自己submitted_by的任务(Skill Owner 的紧急权除外)。UPLOADED可由原作者或 Owner显式 withdraw 为WITHDRAWN;confirm 后才更新 canonical metadata、latest_version_id、published_by。latest_version_id始终只指向PUBLISHEDattempt,不能指向SCANNING/UPLOADED/PENDING_REVIEW/REJECTED/WITHDRAWN/YANKED。latest_version_id和 ReviewTask/Version 状态必须保持一致。review_task.submitted_by或skill_version.created_by。即使用户同时为 Maintainer 和 Namespace ADMIN/Reviewer 也不能自审;首期不提供绕过四眼原则的 SUPER_ADMIN 内容发布捷径。8. Web、查询与通知
owned UNION maintained,稳定按updated_at DESC, id DESC排序并去重;支持 Owned/Maintaining 筛选,显示 Owner 与关系 badge。relationships[]和细粒度capabilities;Maintainer 可看到 private/draft/scan/review 预览,但看不到 Owner 专属按钮。skillId + namespace/slug + Owner,validate/publish 始终携带同一个 ID/ETag。权限在页面打开后被撤销时,服务端拒绝,UI 刷新 capability,不得回退到 legacy publish。持久化 outbox/可重试消费者至少覆盖:
SKILL_MAINTAINER_GRANTEDSKILL_MAINTAINER_REVOKEDSKILL_MAINTAINER_AUTO_REVOKEDSKILL_OWNER_TRANSFERREDSKILL_OWNER_RECOVERY_OVERRIDE通知收件人按事件定义为 Owner + 当前有效 Maintainers(排除 actor、去重、排除 inactive);ACL 变更通知目标用户与 Owner;审核结果至少通知
created_by、submitted_by和 Owner;break-glass 通知旧/新 Owner和当前 Maintainers。发布事件必须明确携带authoredBy/submittedBy/reviewedBy/publishedBy/ownerId,不能继续假设 publisher 等于 Owner。订阅者仍只在真正 PUBLISHED 后收到新版本通知。通知只在事务提交后发送,可重试且不能影响授权正确性;同一事件必须有唯一 event ID 防重复。9. 审计、安全与可观测性
授权、Owner 恢复及所有协作 mutation 写入不可变审计,至少包含:
skillId/namespaceId/versionId;ownerId;actorUserId、target user、grantor/revoker;不得记录 token、完整包内容或敏感文件。包内 metadata 不能改变 Owner/namespace/visibility;所有 IDOR、伪造
skillId、跨 namespace、非 member、inactive 用户、缺 token scope 场景必须 fail closed。结构化日志/trace 覆盖 target resolution → policy → staging → finalize → review/outbox,关键指标包括:
TARGET_SKILL_REQUIRED、shadow-skill 防护、active-work conflict;SCANNINGage、watchdog retry/exhausted、迟到结果丢弃和 scan active-slot recovery;上线前必须有 dashboard、告警和 runbook。以下至少触发停止放量:ACL 一致性巡检非零、出现影子 Skill、DB constraint/deadlock 或 orphan artifact 持续增长、owner legacy publish 成功率显著低于基线、目标发布 5xx 或延迟超过既定 SLO。具体绝对/相对阈值由运维基线记录在 runbook,不能在全量后补。
10. CLI 与 workspace sync(关联 #724)
--skill-id <id>(可保留--target作为 alias);维护现有 Skill 时为必需。dry-run 分别输出localPackageFingerprint、skillRevision/ETag、remoteVersion/remoteFingerprint,不能用一个 fingerprint 字段混淆;JSON 字段 additive,错误有稳定非零 exit code。GET /api/cli/v1/namespaces/{namespace}/skills?page=&size=(installable manifest,仅返回调用者可读的 PUBLISHED;匿名只含 PUBLIC,认证读取 restricted 需skill:read)和GET /api/cli/v1/namespaces/{namespace}/manageable-skills?page=&size=(必须认证且有skill:read+ restricted ACL,返回用户可维护目标及 capability)。二者都返回分页、schemaVersion=2;manageable item 至少含skillId/ownerId/namespace/slug/relationship/capabilities/skillRevision、headAttempt{version,attemptNo,status,fingerprint}与published{version,fingerprint}。sync 的remoteVersion/remoteFingerprint明确定义为 pull 时的 headAttempt,不与 published fingerprint 混用,也不能泄露无权维护的草稿。sync push的 validate 和 publish 必须使用 lockfile 中同一skillId + expectedSkillRevision + expectedRemoteVersion/Fingerprint;本地包另算localPackageFingerprint。同 namespace 有多个 Owner 的同 slug Skill 时,只更新 lockfile 指向的 Skill;缺 ID/歧义/撤权一律非零退出且零写入,绝不能创建 actor 的影子 Skill。rejectExistingVersion=true:只要该 target 已存在相同 semantic version 的任一 attempt 就返回 409,不创建重试 attempt;普通手工发布若不设置该字段,才按第 7 节的 immutable-attempt 规则处理。publish response 必须包含上一节冻结的status等字段。--continue-on-error,必须明确是 opt-in。sync push。在此之前,feat(cli): add namespace workspace sync #724 即使先合并 pull/status/diff,也必须用独立 feature flag 禁用 push,不能依赖尚未进入main的接口假设。11. Owner 之外的特殊入口
publisherId查 Skill 的路径。12. 实施拆分
建议按可独立评审的 PR 拆分:
SkillAccessPolicy、Maintainer API、成员移除/禁用/账号合并、Owner transfer/recovery、审计、锁顺序与 feature flag。--skill-id、两个 manifest endpoint、lockfile schema、feat(cli): add namespace workspace sync #724 server-first 适配和真实 E2E。前置 PR 的接口需先落 OpenAPI/ADR;后续 PR 不得临时改变权限矩阵或目标解析语义。
Alternatives Considered
Impact
Migration / deployment / rollback
skill_version.attempt_no回填为 1、skill.revision回填为 0,并按第 3 节 dual-read nullablebundle_storage_key/published_by;V43 → 全部新增迁移和全新安装都必须在 PostgreSQL 实测。Release-blocking test matrix
created_by、普通 withdraw 只认submitted_by、Skill Owner 紧急 withdraw 要求 reason。skill.id/owner_id不变,只存在一条 aggregate;created_by/submitted_by/reviewed_by/published_by/skill.updated_by分别等于真实制品作者、提审者、审核者、发布转换 actor、最后 Skill mutator。重试生成 attempt N+1,旧制品/审核证据不变;包伪造 Owner/visibility/slug 均失败。SCAN_FAILED、人工 withdraw 与回调竞态,均不得复活 attempt 或永久占用 active slot。UPLOADED不误判发布且published_at/latest_version_id正确归一、bundle dual-read、两阶段 active index、最低兼容回退基线、蓝绿、forward rollback/re-enable;巡检 0 异常;dashboard/alerts/runbook;requestId 可从 API trace 追到 audit/version/outbox。最终 release gate(Given/When/Then):
skillId/ownerId;skillId更新指定 Skill;SCAN_FAILED释放 active slot;随后新 attempt 可发布,迟到扫描结果不能复活旧 attempt。Contract Or SDK Impact
Idempotency-Key/ETag 语义;进入 OpenAPI、生成 SDK、operator docs 和 i18n。skillId、ownerId、relationships[]、细粒度capabilities、operation/三个独立 precondition/revision;但新增WITHDRAWN/attempt 语义要求先落可容忍未知状态和 UUID bundle key 的最低兼容 server/client 基线。--skill-id、JSON 字段和确定 exit codes;workspace manifest/lockfile 升 schema version并提供显式迁移策略。