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:
parent
810f2df8be
commit
d41b986cb0
7 changed files with 159 additions and 30 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
|
@ -99,6 +100,36 @@ func requestUserID(r *http.Request) string {
|
|||
return r.Header.Get("X-User-ID")
|
||||
}
|
||||
|
||||
// resolveActor determines whether the request is from an agent or a human member.
|
||||
// If X-Agent-ID and X-Task-ID headers are both set, validates that the task
|
||||
// belongs to the claimed agent (defense-in-depth against manual header spoofing).
|
||||
// If only X-Agent-ID is set, validates that the agent belongs to the workspace.
|
||||
// Returns ("agent", agentID) on success, ("member", userID) otherwise.
|
||||
func (h *Handler) resolveActor(r *http.Request, userID, workspaceID string) (actorType, actorID string) {
|
||||
agentID := r.Header.Get("X-Agent-ID")
|
||||
if agentID == "" {
|
||||
return "member", userID
|
||||
}
|
||||
|
||||
// Validate the agent exists in the target workspace.
|
||||
agent, err := h.Queries.GetAgent(r.Context(), parseUUID(agentID))
|
||||
if err != nil || uuidToString(agent.WorkspaceID) != workspaceID {
|
||||
slog.Debug("resolveActor: X-Agent-ID rejected, agent not found or workspace mismatch", "agent_id", agentID, "workspace_id", workspaceID)
|
||||
return "member", userID
|
||||
}
|
||||
|
||||
// When X-Task-ID is provided, cross-check that the task belongs to this agent.
|
||||
if taskID := r.Header.Get("X-Task-ID"); taskID != "" {
|
||||
task, err := h.Queries.GetAgentTask(r.Context(), parseUUID(taskID))
|
||||
if err != nil || uuidToString(task.AgentID) != agentID {
|
||||
slog.Debug("resolveActor: X-Task-ID rejected, task not found or agent mismatch", "agent_id", agentID, "task_id", taskID)
|
||||
return "member", userID
|
||||
}
|
||||
}
|
||||
|
||||
return "agent", agentID
|
||||
}
|
||||
|
||||
func requireUserID(w http.ResponseWriter, r *http.Request) (string, bool) {
|
||||
userID := requestUserID(r)
|
||||
if userID == "" {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue