Skip to content

[Feature] Skill Maintainer:支持团队多人协作维护与发布(生产级完整方案) #732

Description

@jiahao6635

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 目标

  1. skill.owner_id 仍是唯一所有者,不改为多人 Owner,也不改变 (namespace_id, slug, owner_id) 唯一约束。
  2. Maintainer 是 Skill 级授权,不扩展 NamespaceRole,也不把 Skill 改成 namespace 共有资源。
  3. 首期只在 TEAM namespace 开启;TEAM 下 PUBLICNAMESPACE_ONLYPRIVATE 均可授权,但 Maintainer 发布时 visibility 由目标 Skill 决定,不能借上传改变 visibility。Builtin/GLOBAL Skill 不接受 Maintainer;promotion 产生的 GLOBAL 副本不继承源 Skill 的 Maintainer。
  4. 所有协作 mutation 必须显式携带 targetSkillId;禁止仅凭 namespace/slug、Owner 名称或“唯一匹配”猜目标。
  5. 服务内部必须分离 actorUserIdtargetSkillIdownerId:Owner 从目标 Skill 读取,客户端不能提交/伪造。skill_version.created_by 是制品作者,review_task.submitted_by/reviewed_by 是提审/审核者,skill_version.published_by 是触发最终发布的人或系统,skill.updated_by 只表示最后修改 Skill 的 actor,四者不得互相代填。
  6. Namespace ADMIN/OWNER 保留现有治理能力,但不会因 namespace 角色自动获得“向他人 Skill 上传任意新内容”的权限。本方案不给 SUPER_ADMIN 新增 target content write 或 grant 能力;首期仅保留审计化 Owner recovery,以及现有平台治理入口。未来如需内容 break-glass,应单独设计入口和二次授权。
  7. Maintainer 授权本身不产生审核批准权;用户同时具有 Maintainer 和 Reviewer/Admin 身份时,能力取并集,但不能审核自己创建或提交的版本。
  8. 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 重新判定。建议能力至少拆成 canReadRestrictedcanPublishVersioncanRetryOwnVersioncanWithdrawOwnVersioncanSubmitReviewcanConfirmPrivatecanRereleasecanManageMaintainerscanChangeVisibilitycanArchivecanDeleteSkillcanReviewcanTransferOwnership,不再用单一 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 1supersedes_version_idartifact_fingerprintreview_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.WITHDRAWNReviewTaskStatus.WITHDRAWN。只有从未进入 scan/review 的 DRAFT 可直接物理删除;其他删除按 retention 软删除/撤回,最终清理由审计化保留策略执行。
  • skill_owner_transfer 至少包含 skill、from_owner_id/to_owner_idexpected_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 读取;
  • VisibilityCheckerSkillQueryServiceSkillLifecycleProjectionService、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=UPDATEskillIdownerIdauthorizedAsresolvedSlugresolvedVersionlocalPackageFingerprintskillRevision/ETag、当前 remote version/fingerprint。publish 响应固定包含 skillId/versionId/attemptNo/version/status/ownerId/createdBy/publishedBy/skillRevision。真实 publish 始终重新解析包和鉴权,不能把 validate 当授权凭证。

发布写接口使用独立 Idempotency-KeyX-Request-ID 只做 trace):key 作用域为 actor + route + target,并绑定 request hash。Session 状态为 STAGING -> FINALIZING(lease) -> FINALIZEDFAILED/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_REQUIREDSKILL_TARGET_NOT_FOUNDSKILL_MAINTAINER_REQUIREDSKILL_TARGET_MISMATCHSKILL_VERSION_CONFLICTSKILL_ACTIVE_SUBMISSION_EXISTSSKILL_PRECONDITION_FAILED(412)、REMOTE_CONTENT_CHANGEDMAINTAINER_NOT_NAMESPACE_MEMBERMAINTAINER_LIMIT_REACHEDIDEMPOTENCY_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_idskill_idactor_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_keyskill_version.bundle_storage_key 保存实际 UUID key,GC 删除前必须反查确认没有任何 DB 引用。失败/过期对象由 TTL GC 删除,删除失败进入现有 storage compensation 重试并告警。

finalize 的短事务必须:

  1. 所有相关写事务只采用这一套全局锁顺序:user_account(ID 排序) -> namespace -> namespace_member(ID 排序) -> skill(skillId 排序) -> skill_maintainer -> skill_owner_transfer -> upload_session -> skill_version -> review_task。不需要的层级直接跳过,但不得反向加锁;批量成员移除、账号禁用和多 Skill 操作也必须按 ID 排序。检测到 serialization/deadlock 时仅做有界重试。
  2. 实时重查 actor 状态、namespace/membership/ACL、Skill status、package slug/visibility、If-Match,并要求当前 skill.revision == session.expected_skill_revision。因此 revoke、member removal、disable、transfer 或 regrant 后,撤权前创建的 session 永久失效,不能因重新授权复活。
  3. 创建新的 immutable attempt(绝不原地覆盖旧制品)、文件引用、真实 created_by、review submission intent、审计和持久化 outbox,并在同一事务将 session FINALIZED;提交后才触发 scan/通知。finalize 不更新 latest_version_id 或 canonical display name/summary;只有后续 PUBLISHED 状态转换事务可更新它们并写 published_by/updated_by
  4. 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 定义为 SCANNINGUPLOADEDPENDING_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_atSCANNING:先以 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_bysubmitted_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_idpublished_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_byskill_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_bysubmitted_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 分别输出 localPackageFingerprintskillRevision/ETagremoteVersion/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/skillRevisionheadAttempt{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 拆分:

  1. Compatibility baseline + expand schema:dual-read bundle、容忍新状态/attempt、revision/upload session/transfer/outbox 表和回填;功能关闭。
  2. Policy + ACL lifecycle:集中式 SkillAccessPolicy、Maintainer API、成员移除/禁用/账号合并、Owner transfer/recovery、审计、锁顺序与 feature flag。
  3. 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 的确定冲突映射。
  4. Data remediation + invariant index:生产预检/修复工具与审计,随后独立 non-transactional/维护窗口 migration 创建 active-work unique index。
  5. Read model + Web:restricted read、My Skills owned/maintained、capabilities、缓存失效、管理/发布 UI。
  6. CLI + sync--skill-id、两个 manifest endpoint、lockfile schema、feat(cli): add namespace workspace sync #724 server-first 适配和真实 E2E。
  7. Rollout/operations:dashboard、alerts、巡检/GC/runbook、容量/查询计划验证、兼容基线/forward rollback 和全链路演练。

前置 PR 的接口需先落 OpenAPI/ADR;后续 PR 不得临时改变权限矩阵或目标解析语义。

Alternatives Considered

  1. 扩展 NamespaceRole:权限范围过大,会让成员维护 namespace 内所有 Skill,无法表达逐 Skill 委托;不采用。
  2. 多人 Owner / namespace 共有 Skill:会改变现有 Owner 隔离、唯一约束、查询和治理语义,迁移风险高;不采用。
  3. 删除 Owner 唯一维度:会破坏既有同 slug/不同 Owner 数据;不采用。
  4. 按 namespace/slug/当前用户自动推断目标:同 slug 可多条,actor 也可能同时拥有和维护不同 Skill,无法安全消歧;不采用。
  5. 让 Namespace ADMIN 自动发布任意内容:治理角色不应默认成为供应链内容写入者;不采用。
  6. 上传期间持有数据库锁:对象存储延迟会扩大锁时间和故障半径;采用 staging + 短 finalize 事务。
  7. 只改 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):

  1. Owner 发布 v1,并将一个 active namespace MEMBER 授权为 Maintainer;
  2. 对方在 My Skills 看到 Maintaining,dry-run 明确显示 UPDATE 到原 skillId/ownerId
  3. Maintainer 发布 v2,Owner 不变、actor/audit/notification 正确、没有影子 Skill;
  4. 普通 MEMBER 不能读 private/draft 或写入;Maintainer 不能转授权、改 visibility、硬删、治理或自审;
  5. Owner revoke 后,新的 publish/finalize 立即失败;重新授权后移除 namespace membership,ACL 自动清理且仍失败;
  6. 两个 Maintainer 同版本并发只有一个成功,另一个 409,无 500/孤儿对象/错误 latest pointer;
  7. Owner 失效时 Maintainer 仍可内容维护,Namespace OWNER 可审计化恢复 Owner,普通 ADMIN 不可;
  8. 旧 Owner CLI 发布自己的 Skill 继续成功;feat(cli): add namespace workspace sync #724 sync 仅通过 lockfile skillId 更新指定 Skill;
  9. feature flag 关闭和重新开启演练通过,ACL/版本/审计数据无损。
  10. 模拟扫描消息永久丢失后,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 扩展:skillIdownerIdrelationships[]、细粒度 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 身份暴露到公共搜索/下载协议。

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort/l大改动或高风险改动,需要 maintainer 负责 / Large or risky change requiring maintainer ownership.priority/p1高优先级 / High priority triage bucket.risk/high涉及安全、鉴权、迁移或公共契约 / Touches security, auth, migrations, or public contracts.triage/core交由 core maintainer 结合 AI 协同处理 / Issue should be handled by a core maintainer with AI support.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions