4 Commits

Author SHA1 Message Date
Bohan Jiang
b26f850d4e feat(agents): gate private-agent surfaces with allowed_principals predicate (#2359)
* feat(agents): gate private-agent surfaces with allowed_principals predicate

Tighten chat/@-mention, history, edit, and delete entry points so private
agents are only reachable by their owner or workspace owner/admin. Agent-to-
agent traffic still bypasses the gate so A2A collaboration keeps working.

- New canAccessPrivateAgent predicate in handler/agent_access.go; used by
  comment.enqueueMentionedAgentTasks (replacing the inline check), GetAgent,
  ListAgents (filter), ListAgentTasks, GetWorkspaceAgentRunCounts /
  Activity30d / TaskSnapshot (workspace-wide aggregations no longer leak
  private-agent existence + counts), chat.CreateChatSession,
  chat.SendChatMessage (re-checks on every send so role changes can't leave
  a stale session as a back-door), and autopilot.shouldSkipDispatch
  (caller = autopilot creator).
- allowed_principals is computed inline as {agent.owner_id} ∪ workspace
  owner/admin members. No new table — manual config is intentionally not
  exposed in v1; the predicate is the extension seam.
- Front-end agent detail page distinguishes 403 (private agent the caller
  can't access) from 404 (deleted/missing) and renders a "no access"
  placeholder with a back-to-agents button.
- Go tests cover the pure predicate matrix + the four protected surfaces;
  vitest passes for the affected views.

Co-authored-by: multica-agent <github@multica.ai>

* feat(agents): gate issue assignment with the private-agent predicate

Refactor validateAssigneePair to call the shared canAccessPrivateAgent
helper. This closes the back door where a plain member could assign a
private agent to an issue and let normal task dispatch run it, side-
stepping the chat / @-mention gate. Agent callers (X-Agent-ID) bypass
so A2A delegation onto a private assignee still works.

Add an integration test covering all three callers (workspace owner,
agent owner, plain member).

Co-authored-by: multica-agent <github@multica.ai>

* fix(agents): close three private-agent gate bypasses found in PR review

1. X-Agent-ID forgery (resolveActor): require X-Task-ID alongside
   X-Agent-ID before trusting the agent identity. Without this a plain
   workspace member could set X-Agent-ID to any visible agent UUID and
   short-circuit the gate to "actor=agent, allow". Daemons already
   pair the two headers, so legitimate A2A traffic is unaffected.

2. Chat history read path (chat.go): GetChatSession / ListChatMessages /
   GetPendingChatTask / MarkChatSessionRead now go through a new
   gateChatSessionForUser helper that re-applies canAccessPrivateAgent
   after the ownership check, so a session creator whose role was later
   downgraded loses transcript access. ListChatSessions and
   ListPendingChatTasks filter their result sets by the same predicate.

3. Cross-workspace @mention (comment.enqueueMentionedAgentTasks):
   resolve the mentioned agent via GetAgentInWorkspace scoped to the
   issue's workspace so a UUID belonging to a different workspace's
   private agent can't slip past the gate (the gate was being applied
   against the current workspace's role table, which is the wrong
   one).

Regression tests cover each bypass, plus an update to the resolveActor
unit test to reflect the new "X-Agent-ID without X-Task-ID falls back
to member" contract.

Co-authored-by: multica-agent <github@multica.ai>

* test(handler): seed X-Task-ID alongside X-Agent-ID in existing agent-caller tests

After tightening resolveActor to require both headers (X-Agent-ID +
X-Task-ID) for the "agent" actor identity, three existing tests that
set only X-Agent-ID started failing because their requests now resolve
to "member" instead of "agent". Add createHandlerTestTaskForAgent
helper and seed a task per agent-caller assertion. Also patch
TestAgentExplicitMentionStillTriggers — it still passed only because
the @mention path doesn't care about author type for member callers,
but the test claims to exercise the agent path, so make it faithful.

Co-authored-by: multica-agent <github@multica.ai>

* test(handler): finish X-Task-ID seeding + fix cross-workspace mention test schema

The previous CI run still failed in two places:

1. server/cmd/server integration tests — postCommentAsAgent → authRequestWithAgent
   only set X-Agent-ID, so resolveActor downgraded the request to "member"
   and the on_comment chain produced the wrong task counts. Fix:
   authRequestWithAgent now also sets X-Task-ID, fetched or seeded by a new
   ensureAgentTask(agentID) helper.

2. TestMentionAgent_RejectsCrossWorkspaceAgentUUID's hand-crafted comment
   INSERT was missing comment.workspace_id, which migration 025 made
   NOT NULL. Pass testWorkspaceID into the seed row.

Build + vet clean locally; both packages compile.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-11 12:39:45 +08:00
Bohan Jiang
9b45e0d4a6 feat(cli): add issue subscriber commands (#1265)
* feat(cli): add `issue subscriber` commands

Wrap the existing /subscribers, /subscribe, and /unsubscribe endpoints as
`multica issue subscriber list|add|remove`, mirroring the comment subcommand
shape. `--user <name>` reuses resolveAssignee to resolve a member or agent;
without the flag, the action targets the caller.

* fix(issues): default subscribe target to resolveActor, not X-User-ID

When no user_id is posted, subscribe/unsubscribe hardcoded the target as
("member", X-User-ID). A CLI caller running as an agent (X-Agent-ID set)
then subscribed the underlying member rather than the agent itself,
which contradicts the "defaults to the caller" contract.

Derive the default via resolveActor so the endpoint mirrors caller
identity consistently — agent caller → agent row, member caller →
member row. Adds a regression test covering the agent caller path.
2026-04-17 16:26:00 +08:00
devv-eve
8c0708bb5d fix(server): validate workspace membership for subscriptions and uploads (#935)
* fix(server): validate workspace membership for subscription targets and file uploads

Closes MED-1 (cross-workspace subscription injection) and MED-2 (file upload
missing workspace member validation) from the security audit.

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

* test(server): add negative tests for cross-workspace subscription and upload

Address PR review feedback:
- Add tests verifying cross-workspace user_id is rejected with 403 on
  subscribe and unsubscribe
- Add test verifying upload with foreign workspace_id is rejected with 403
- Make isWorkspaceEntity explicitly enumerate "member"/"agent" and reject
  unknown user types

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

---------

Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:22:03 -07:00
Naiyuan Qing
bfe9498def feat(notifications): replace hardcoded inbox notifications with subscriber-driven model
Replace inbox_listeners.go with a subscriber-driven notification system:

- Add issue_subscriber table with auto-subscribe on create/assign/comment
- New subscriber_listeners.go: maintains subscriber data on domain events
- New notification_listeners.go: notifySubscribers (fanout to all subscribers
  minus actor) and notifyDirect (targeted, punches through unsubscribe)
- Subscriber API: list/subscribe/unsubscribe endpoints
- Frontend: subscribers section in issue detail sidebar with real-time sync
- Frontend: inbox notification grouping by (issue_id, type, actor_id)
- Remove createInboxForIssueCreator from task.go (unified through event bus)
- 21 new Go tests, all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 19:33:20 +08:00