issue create --attachment-id 的绑定 SQL 要求 附件上传者 == Issue 创建者。
图是人类成员上传的,Issue 是 agent 创建的,两者不相等 → UPDATE 命中 0 行 → 绑定静默失败,
而 Issue 照常创建成功、退出码 0、无任何报错。attachment download 返回 404 —— 是
任务作用域造成的假阴性。
服务端把「Issue 创建者」当成「附件上传者」去过滤要绑定的附件。对 agent 建单场景,这个等式永远不成立。
attachment 行上的 uploader_type / uploader_id,在文件上传那一刻写死,指向真正把字节传上来的主体。人类在 chat 里贴图 → member。issue.creator_type / creator_id,由服务端按当前 task 的 actor 解析。agent task 里跑 CLI → agent。不能由客户端 env 伪造(实验 3 已验证)。attachments 字段,没有任何 stderr、warning 或错误码。调用方(含 agent)无法从接口观测到失败。multica attachment download 只能取到授予当前 task 的附件。对其它 task 的活附件一律返回 404 —— 这是权限边界,不是存在性判定。前序结论:「attachment id 019fdc2e-… 在服务端已失效/已删除,server 静默跳过绑定」。它的唯一证据是一次 404。
$ multica attachment download 019fdc2e-cceb-765b-abc5-f5bd713471bd
The requested resource was not found.
我把这个方法用在两个已被 issue get 证明活着的附件上,得到同样的 404:
| attachment id | 服务端真实状态 | download 结果 |
|---|---|---|
019fdc33-a8eb-7588-… | 存活,绑在 BUG-5,有 download_url | rc=4 not found |
019fdc34-499e-7a9e-… | 存活,绑在 BUG-4,有 markdown_url | rc=4 not found |
019fdc2e-cceb-765b-… | 存活(见下方直接反证) | rc=4 not found |
所以 404 只能推出「本 task 没被授予这个附件」。更直接的反证 —— 我用同一个 id 成功绑定:
$ multica issue create … --attachment-id 019fdc2e-cceb-765b-abc5-f5bd713471bd
→ BUG-8, attachments: [{
"id": "019fdc2e-cceb-765b-abc5-f5bd713471bd",
"filename": "img_v3_0214b_42ecb616-…-44831a71308g.gif",
"created_at": "2026-08-07T20:24:42+08:00",
"chat_session_id": "5d037921-3062-4f3b-a8ef-52760dfd2a49",
"uploader_type": "member",
"uploader_id": "9e8a3719-73f1-4435-8223-71ffb6054967",
"size_bytes": 2825604
}]
该附件从创建至今一直存活、一直可绑定。用户「结论完全是错的」这个判断是对的。
BUG-5 是以成员身份创建的(creator_type: member),而它绑的附件 uploader 也是同一个成员 —— 恰好满足 uploader == creator。所以它绕开了真正的失败条件。
那次实验同时更换了两个变量(attachment id 不同 + 主体身份不同),不是对照实验。它得出「换个 id 就行 → 所以老 id 坏了」,把身份这个真变量归因到了 id 上。
~/Library/Caches/trae-cli/sessions/258f3c92-73cc-4b3a-b9c1-689b8ec8912f/events.jsonl
Attachments on this message:
- id=019fdc2e-cceb-765b-abc5-f5bd713471bd filename="img_v3_…gif" content_type=image/gif
Use `multica attachment download <id>` to fetch each file locally before referring to it.
When creating an issue that should preserve one of these attachments, pass
`--attachment-id <id>` to `multica issue create` in addition to keeping the
attachment markdown inline.
# 20:25:36 第一次 —— 被去重守卫拒绝
multica issue create --title "[v0.0.3 Bug] mermaid 有 bug" \
--description-file ./description.md --status backlog \
--attachment-id 019fdc2e-cceb-765b-abc5-f5bd713471bd --output json
→ Active duplicate issue exists: BUG-2 …
# 20:25:41 第二次 —— 加 --allow-duplicate,成功
multica issue create --title "[v0.0.3 Bug] mermaid 有 bug" \
--description-file ./description.md --status backlog \
--attachment-id 019fdc2e-cceb-765b-abc5-f5bd713471bd \
--allow-duplicate --output json
→ {"identifier":"BUG-3",
"creator_type":"agent",
"creator_id":"cada05b8-b1fd-48a8-8d04-450cdd7d8d9f", … }
# 返回体里完全没有 attachments 字段
参数一字不差,没有报错,没有 hallucinate。唯一异常是返回体静默缺少 attachments。关键字段就是那行 creator_type: "agent"。
以下均取自部署中的 commit 520e45a2e(与 CLI v0.0.2 一致),仅只读查看,未改动任何代码。
server/internal/service/issue.go:294
func (s *IssueService) linkAttachments(ctx context.Context, issue db.Issue, ids []pgtype.UUID) []db.Attachment {
if len(ids) == 0 { return nil }
if err := s.Queries.LinkAttachmentsToIssue(ctx, db.LinkAttachmentsToIssueParams{
IssueID: issue.ID,
WorkspaceID: issue.WorkspaceID,
UploaderType: issue.CreatorType, // ← 把「Issue 创建者」当成「附件上传者」
UploaderID: issue.CreatorID, // ←
AttachmentIds: ids,
}); err != nil {
slog.Error("failed to link attachments to issue", …) // 只记日志,不影响返回
return nil
}
…
}
函数注释本身就写明这是「best-effort post-commit step」—— 设计上就是失败也不告诉调用方。
server/pkg/db/queries/attachment.sql:340
-- name: LinkAttachmentsToIssue :exec
UPDATE attachment
SET issue_id = sqlc.arg(issue_id),
expires_at = NULL
WHERE workspace_id = sqlc.arg(workspace_id)
AND issue_id IS NULL -- 条件 A:必须尚未绑定任何 issue
AND uploader_type = sqlc.arg(uploader_type) -- 条件 B:上传者类型 == Issue 创建者类型
AND uploader_id = sqlc.arg(uploader_id) -- 条件 C:上传者 id == Issue 创建者 id
AND id = ANY(sqlc.arg(attachment_ids)::uuid[]);
| 字段 | BUG-3 实际值 | 条件 B 判定 |
|---|---|---|
| 附件 uploader_type / uploader_id | member / 9e8a3719-…(人类) | 'member' = 'agent'→ false → 0 rows |
| BUG-3 creator_type / creator_id | agent / cada05b8-…(提单助手) |
仓库里有专门的安全测试 server/internal/handler/attachment_link_security_test.go → TestLinkAttachmentsToIssueRequiresOriginalUploader,防止「成员 A 猜到成员 B 的 attachment UUID 就把它据为己有」。file.go:1499 的注释也写明:「Only the original uploader may claim an unbound staged attachment」。
问题在于这条规则没有为「agent 代人类建单」开口子,而平台的 prompt 又恰恰要求 agent 走这条路。安全意图正确,覆盖面不完整。
server/internal/daemon/prompt.go:398 —— 生成那句「pass --attachment-id」指示,条件是 task.TargetType != taskTargetRuntime。server/cmd/multica/cmd_issue.go:1148 —— CLI 把 --attachment-id 与 MULTICA_QUICK_CREATE_ATTACHMENT_IDS 合并进 body["attachment_ids"]。server/internal/handler/issue.go:2286 / 2399 / 2408 —— 解析 attachment_ids,并把 CreatorType 设为当前 actor。server/internal/handler/issue.go:2668-2687 —— issue update 走 resolveActor + 同一个 query,对 agent 同样失效。全部以成员身份在 workspace 470e8a4a-… 执行。探测 issue 一律 --status cancelled、无 assignee,不上看板、不触发任何 agent。
| # | 操作 | 实测结果 | 证明了什么 |
|---|---|---|---|
| 1 | create --attachment-id 019fdc2e-…(成员建单,uploader 也是该成员) |
BUG-8,attachments 有 1 条 | 附件存活且可绑定 → 「已失效」结论被推翻 |
| 2 | 同一次 create 里附带--attachment-id 00000000-0000-0000-0000-000000000000 |
静默丢弃,无报错,rc=0 | --attachment-id 无任何校验反馈,不可解析的 id 一律静默吞掉 |
| 3 | MULTICA_AGENT_ID=cada05b8-… create --attachment-id 019fdc2e-… |
BUG-9,creator_type 仍是 member,attachments 为 null | agent 身份不能由客户端 env 伪造(服务端按 task 解析 actor);此处 null 由条件 A 造成 |
| 4 | 去掉 env 重跑同一 id(对照) | BUG-10,同样 attachments 为 null | 实验 3 的 null 与 MULTICA_AGENT_ID 无关 —— 它是干扰项,必须排除 |
| 5 | 三个已知存活附件全部 attachment download |
全部 rc=4 not found | download 404 是任务作用域假阴性,不能当作附件不存在的证据 |
实验 3+4 顺带确认了条件 A 的真实语义:附件一旦绑定到某个 issue,后续 --attachment-id 传同一个 id 会被静默丢弃。一个附件只能绑一个 issue。
这是用户的核心疑问。答案是两条路径的 SQL 条件完全不同 —— 评论侧根本没有 uploader 过滤。
server/pkg/db/queries/attachment.sql:174
-- name: LinkAttachmentsToComment :exec
UPDATE attachment
SET comment_id = $1, expires_at = NULL
WHERE issue_id = $2
AND comment_id IS NULL
AND id = ANY($3::uuid[]);
-- 注意:没有 uploader_type / uploader_id 条件
而且 comment add --attachment <本地路径> 是先上传一个新文件(uploader = 调用方自己),再绑定 —— 天然满足所有条件。
| 路径 | uploader 过滤 | agent 代人类挂图 |
|---|---|---|
issue create --attachment-id <已有 id> | 有(uploader == issue creator) | 永远失败,且静默 |
issue update --attachment-id <已有 id> | 有(同一个 query) | 同样失败 |
comment add --attachment <本地路径> | 无 | 成功 |
issue create --attachment <本地路径> | 有,但上传者就是调用方 | 成功 |
所以对 agent 而言,唯一可靠的挂图方式是先 multica attachment download <id> 落到 workdir,再用 --attachment <本地路径>(BUG-4 就是这么成功的)。而平台 prompt 却明确指示它用 --attachment-id。
--attachment-id 挂人类上传的附件,100% 失败且无感知。这解释了 BUG-2 的同类现象(同模板、同 agent)。daemon.go:4588 把人类上传的附件 id 经 MULTICA_QUICK_CREATE_ATTACHMENT_IDS 交给 agent,cmd_issue.go:1149 自动并入 attachment_ids,但 issue 由 agent 创建 → 同一个 uploader 不匹配 → 静默丢弃。该 env 的测试(daemon_test.go:4737)只断言「env 被正确传递」,没有断言「附件最终绑上了」。issue update --attachment-id 对 agent 一样失效(issue.go:2668-2687,同一个 query)。linkAttachments 只 slog.Error、handler 不校验请求的 id 是否真的绑上了。agent 因此必然汇报成功 —— 这不是模型幻觉,是接口没有给出可观测的失败。供产品/研发决策,本次未实施任何修复。按优先级:
fea75f5c 里创建 issue,附件 019fdc2e 属于该 task 的 chat session 5d037921 —— 这是合法授权关系。给 LinkAttachmentsToIssue 增加一条「附件的 chat_session_id/task_id 属于当前 task」的放行分支,既修好 agent 场景,又不放开「猜 UUID 偷别人附件」(原安全测试仍然通过,因为陌生成员的 staged 附件不在你的 task 里)。LinkAttachmentsToIssue 改成 :execrows,或返回实际绑定的 id 列表。chat 侧已有现成先例:chat.go:700 的 BoundAttachmentIDs 明确区分「请求的」和「实际绑上的」。CLI 在 attachment_ids 非空而 attachments 为空时应当 warn 到 stderr。prompt.go:398 的指示改成「先 multica attachment download <id>,再用 --attachment <本地路径>」—— 这条路径实测可用,无需动服务端。/Users/park0er/Coding/AgenticAds/Rollica 只做只读的 git show / grep,无 checkout、无提交。cancelled、无 assignee、不触发任何 agent。019fdc2e-…,实验 1 的副作用)。deleteIssue 会 CASCADE 删除附件行并清理 S3 对象(issue.go:2960-2972),那会真正销毁这张原图。9a943be1/workdir/img_v3_…gif),不要去动 BUG-8。