Files
multica/server/cmd
Bohan Jiang 8eb81aa396 fix(daemon): enforce workspace isolation for agent execution (#1235) (#1260)
Phase 0 hotfix for the cross-workspace contamination reported in MUL-1027
/ #1235: an agent running for workspace A ended up commenting on (and
renaming) a two-day-old issue in workspace B.

#1249/#1259 fixed resolution for autopilot tasks and consolidated the
task-workspace resolver, and #1294 populated workspace_id in the claim
response for run_only autopilot tasks. Those closed the known fallthroughs
but the failure mode is still broader: whenever the daemon or server fails
to supply a workspace, the CLI silently falls back to
`~/.multica/config.json`, which is user-global, not workspace-scoped. On a
host running daemons for multiple workspaces, a single gap in workspace
propagation is enough to leak writes across workspaces.

This PR adds three coordinated guards so no single layer's bug can cause a
cross-workspace write:

1. `server/cmd/multica/cmd_agent.go` — `resolveWorkspaceID` detects the
   agent execution context (`MULTICA_AGENT_ID` / `MULTICA_TASK_ID` env,
   both daemon-only markers) and in that context refuses to fall back to
   the user-global CLI config. Human / script usage (no agent env) is
   unchanged: flag → env → config fallback chain still applies.

2. `server/internal/handler/daemon.go` — `ClaimTaskByRuntime` now
   captures the runtime's workspace from `requireDaemonRuntimeAccess` and
   enforces `resolved_task_workspace == runtime_workspace` after the
   existing issue/chat/autopilot branches. On mismatch or empty, the
   handler explicitly cancels the just-dispatched task (via
   `TaskService.CancelTask`, which also reconciles agent status) and
   returns 500. Without the explicit cancel, `ClaimTaskForRuntime` had
   already transitioned the task to 'dispatched' and the agent status to
   'working', so a plain 500 would leave both stuck for the ~5 min
   stale-task sweep window.

3. `server/internal/daemon/daemon.go` — `runTask` refuses to spawn the
   agent when `task.WorkspaceID` is empty (defense-in-depth against
   server bugs and reused workdirs).

Tests:
- `cmd/multica/cmd_agent_test.go`:
  `TestResolveWorkspaceID_AgentContextSkipsConfig` — five subtests
  covering the full fallback matrix (outside agent context still reads
  config; agent context uses env; agent context with empty env returns
  empty; task-id-only marker also counts; requireWorkspaceID surfaces the
  agent-context error message).
- `internal/handler/daemon_test.go`:
  `TestClaimTaskByRuntime_TaskWorkspaceMismatch_CancelsAndRejects` —
  constructs a data-inconsistent task (runtime_id in workspace A,
  issue_id in workspace B) and asserts the handler returns 500 AND
  leaves the task in 'cancelled' state (not 'dispatched').

Phase 1/2 follow-ups (prompt injection of workspace slug, session lookup
workspace filter, cross-workspace audit of agent-facing endpoints,
observability) are out of scope for this PR and tracked separately.
2026-04-21 12:55:12 +08:00
..