feat(server): distinguish agent vs human CLI actions (#181)

* feat(server): distinguish agent vs human CLI actions via X-Agent-ID/X-Task-ID headers

Extract resolveActor helper in handler to centralize agent identity resolution
from X-Agent-ID header with X-Task-ID cross-validation. Fix DeleteComment,
DeleteIssue, and UpdateComment handlers that previously hardcoded "member" as
actor type. Forward MULTICA_TASK_ID as X-Task-ID header from CLI client.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(server): add debug logging and test coverage for resolveActor

Add slog.Debug on agent/task validation failures for easier debugging.
Add TestResolveActor with 5 cases covering member fallback, valid agent,
non-existent agent, valid task, and mismatched task.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
LinYushen 2026-03-30 13:12:59 +08:00 committed by GitHub
parent 810f2df8be
commit d41b986cb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 159 additions and 30 deletions

View file

@ -102,16 +102,7 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) {
}
// Determine author identity: agent (via X-Agent-ID header) or member.
authorType := "member"
authorID := userID
if agentID := r.Header.Get("X-Agent-ID"); agentID != "" {
// Validate the agent exists in this workspace.
agent, err := h.Queries.GetAgent(r.Context(), parseUUID(agentID))
if err == nil && uuidToString(agent.WorkspaceID) == uuidToString(issue.WorkspaceID) {
authorType = "agent"
authorID = agentID
}
}
authorType, authorID := h.resolveActor(r, userID, uuidToString(issue.WorkspaceID))
comment, err := h.Queries.CreateComment(r.Context(), db.CreateCommentParams{
IssueID: issue.ID,
@ -205,8 +196,9 @@ func (h *Handler) UpdateComment(w http.ResponseWriter, r *http.Request) {
}
resp := commentToResponse(comment)
actorType, actorID := h.resolveActor(r, userID, uuidToString(issue.WorkspaceID))
slog.Info("comment updated", append(logger.RequestAttrs(r), "comment_id", commentId)...)
h.publish(protocol.EventCommentUpdated, uuidToString(issue.WorkspaceID), "member", userID, map[string]any{"comment": resp})
h.publish(protocol.EventCommentUpdated, uuidToString(issue.WorkspaceID), actorType, actorID, map[string]any{"comment": resp})
writeJSON(w, http.StatusOK, resp)
}
@ -250,8 +242,9 @@ func (h *Handler) DeleteComment(w http.ResponseWriter, r *http.Request) {
return
}
actorType, actorID := h.resolveActor(r, userID, uuidToString(issue.WorkspaceID))
slog.Info("comment deleted", append(logger.RequestAttrs(r), "comment_id", commentId, "issue_id", uuidToString(comment.IssueID))...)
h.publish(protocol.EventCommentDeleted, uuidToString(issue.WorkspaceID), "member", userID, map[string]any{
h.publish(protocol.EventCommentDeleted, uuidToString(issue.WorkspaceID), actorType, actorID, map[string]any{
"comment_id": commentId,
"issue_id": uuidToString(comment.IssueID),
})