origin/develop、失败告警改为新增、
latest 恢复已发布语义、生命周期矛盾拆开、暴露面按部署形态分档、URL 轴移交 MIA-279。
fix/attachment-bind-authz-parity 分支。已在 origin/develop 基线核实:ActorTaskID / resolveActorTaskID / actor_task_id / warnUnboundAttachmentIDs 全部不存在;LinkAttachmentsToIssue 仍带 uploader_type / uploader_id 两行条件。
因此:失败可观测性是新增项而非「保留」;绑定的修改就是删掉那两行,不涉及 actor_task_id。
另外原分支基于 origin/xisheng,还带了 4 个与附件模型无关的 v0.0.2 release-note commit。已用 git rebase --onto origin/develop origin/xisheng <branch> 只重放 spec commit,甩掉那 4 个,并把 tracking 指向 origin/develop。分支与 worktree 原地保留,工作没丢。
| 操作 | 判定 | 与上游 |
|---|---|---|
| 读取 / 下载 / 预览附件 | 请求者是该附件 workspace_id 的成员 | 一致 |
| 绑定附件到 issue | 同 workspace + 目标 issue_id IS NULL | 一致 |
| 绑定附件到 comment | 附件已属该 issue + comment_id IS NULL | 一致 |
| 往运行中任务的回复写文件 | GetRunningNativeWorkTaskForAttachmentUpload | Rollica 专有(保留) |
| 读取 Work / chat 会话与消息 | 会话创建者(GetWorkSessionForCreator 404 / ListTaskMessagesByUser 403) | 一致,不改 |
LinkAttachmentsToIssue 回归上游三条件形态 —— 在当前基线上就是删掉两行:
UPDATE attachment
SET issue_id = $1
WHERE workspace_id = $2
AND issue_id IS NULL
- AND uploader_type = sqlc.arg(uploader_type) ← 删
- AND uploader_id = sqlc.arg(uploader_id) ← 删
AND id = ANY($3::uuid[]);
--attachment-id、无报错、随后下载并改用 comment add --attachment)。
rollica-work-surface 发布过 Work attachments remain creator-private across every access path。本 change 以 MODIFIED Requirement 显式收窄它:私密性改由会话与消息读取层统一保证,附件层不再重复判定。产品已在知情前提下确认,残余风险与部署形态限定一并写入 spec。
原草稿同一条 requirement 里既说「不得在线删除对象」又说「容器删除连带删对象」,自相矛盾。已拆开:
main.go 移除启动,删 attachment_sweeper.go 与其测试,删查询 DeleteExpiredStagedAttachments。expires_at:UploadFile 不再写入;各 Link / Replace 查询不再清空;移除 StagedAttachmentTTL 与 staged_ttl_secs。attachmentRequiresCreatorScope 供 URL 策略使用。warnUnboundAttachmentIDs + 服务端 dropped-id slog.Warn + 单测。expires_at 列仍存在但无人读写;迁移 245 的触发器仍存在但无害(只把该列置 NULL,已无消费者)。回滚 = 回滚代码。
DROP TRIGGER IF EXISTS trg_agent_promote_avatar_attachment ON agent;
DROP TRIGGER IF EXISTS trg_user_promote_avatar_attachment ON "user";
DROP TRIGGER IF EXISTS trg_workspace_promote_avatar_attachment ON workspace;
DROP TRIGGER IF EXISTS trg_squad_promote_avatar_attachment ON squad;
DROP FUNCTION IF EXISTS promote_avatar_attachment();
DROP INDEX IF EXISTS idx_attachment_staged_expiry;
ALTER TABLE attachment DROP COLUMN IF EXISTS expires_at;
down.sql 重建列(全 NULL)、索引、函数与 4 触发器。列内容不可逆恢复 —— 但阶段一后已无语义消费者。迁移号按 develop 当前最大值 +1 确定,不预先写死。
expires_at IS NOT NULL 的行数,及其中三外键全 NULL 的行数 —— 现行判据下「随时可能被删」的集合。url 被任一 avatar_url 引用的行数 —— 曾经/仍处于误删风险的头像。avatar_url 形如 /api/attachments/{uuid}/download 的 agent/user/workspace/squad 行数 —— 迁移 248 之后仍在产生的坏数据存量。TestLinkAttachmentsToIssueRequiresOriginalUploader(0432ba618 引入)断言非上传者不能绑定 —— 这正是本 change 移除的行为。删除,并在 spec 的 REMOVED Requirements 中记录该契约。
新增覆盖:
UploadFile 不写入 expires_at。Work 会话云盘既有测试须全部保持通过。
| 风险 | 缓解 |
|---|---|
| id 泄漏后 Work 附件可被同 workspace 成员读取 | 产品已知情接受并写入 spec;会话与消息层门禁不变且新增测试锁住 |
| 公开 CDN 无签名部署上暴露面扩大到匿名 | 结论按部署形态分档写入 spec;该形态的 URL 策略归 MIA-279 |
| 阶段二删列不可逆 | 阶段一先移除所有消费者使该列无语义;down.sql 可重建结构;发布说明声明列内容不恢复 |
| 去授权时漏改某个调用点 | 读取调用点逐一对照上游同名函数改写,每处配测试 |
| 孤儿 blob 永久累积 | 产品已接受。后续如需可观测性可加只读报表,不得引入删除路径 |
下列都属于「持久引用与可渲染性」这一条轴,产品决策为与 MIA-279 合并处理,MIA-277 / MIA-278 已在 issue 树上挂到 MIA-279 之下:
url / download_url / markdown_url 改写是否移除,以及按部署形态(CloudFront 签名 / 公开 CDN 无签名 / 本地存储)分别采用什么 URL 策略。POST /api/avatar-upload、UploadAvatar、isAllowedAvatarContentType,让头像回到普通附件上传接口(上游本来就是这样)。server/internal/handler/avatar.go 与 /api/avatars/{sig}/* 签名头像读取路由(MUL-5393 / #6024)。Rollica 当前没有该文件。attachmentRequiresCreatorScope 在授权上的用途,不动它在 URL 策略上的用途。因此两条轴互不阻塞,MIA-279 的独立快修也不受影响。