Feature/introduce global agents - #367
Conversation
- Added support for global agents, allowing them to be managed independently of projects. - Updated the ProjectHandler to enforce that either user_id or agent_id must be provided when adding a member. - Enhanced authorization middleware to handle permissions for global agents, ensuring they are checked against their own roles rather than a shared bot subject. - Implemented tests to validate the new global agent permissions and ensure correct behavior in various scenarios. - Updated API routes to include CRUD operations for global agents and their associated resources. - Modified database schema to accommodate global agents, including new fields and constraints. - Enhanced real-time event handling to support global agent chat events.
… actor user ID support - Removed unused imports and components from the agents index page. - Simplified the AgentCard component by eliminating unnecessary logic and UI elements. - Updated the build_mcp_config function to accept an optional actor_user_id parameter for better tracking of actions performed on behalf of users. - Modified the run_conversation function to pass the actor_user_id when building the MCP configuration. - Added tests to ensure the correct inclusion and omission of the actor_user_id in the MCP server configuration. - Enhanced the project handler to attribute project creation to the correct user when an actor user ID is provided. - Implemented middleware changes to parse and store the actor user ID from requests, ensuring proper context handling for agent-authenticated requests.
- Implemented CountDistinctAgentsByProjects in MemberRepository and corresponding service methods to aggregate distinct agents across multiple projects. - Added CountOpenTasksByProjects in TaskRepository and service to count non-done tasks across specified projects in a single query. - Updated ProjectHandler to utilize new counting methods for workspace statistics, replacing previous per-project queries. - Modified related tests to ensure coverage for new counting functionalities and validate correct behavior in aggregate queries.
…mplementations in tests
… project-specific agent retrieval
There was a problem hiding this comment.
Review summary
This PR introduces a major new feature — global agents scoped to the whole instance alongside project-owned agents — and does a lot of things well: the migration is existence-guarded and backfill-safe, the authz middleware cleanly adds a global scope path, and the realtime/ai-agent plumbing correctly routes global-chat events to per-user rooms when project_id is absent.
However, I found a critical authorization gap in the global-chat conversation endpoints that must be fixed before merge: any authenticated user can read, stop, pause, heartbeat, and send messages to any other user's global conversation by knowing its UUID. I also flagged an important security concern around the unverified X-Actor-User-ID header used by agent API key authentication, and a smaller UX inconsistency between listing and detail views for invited global agents.
Required changes
-
Global conversation ownership checks —
GetGlobalConversation,StopGlobalConversation,PauseGlobalConversation,GlobalConversationHeartbeat,SendGlobalConversationMessage, and the events endpoint must verify the caller'susers.idmatches the conversation'sactor_user_id. The cleanest fix is to pass the caller's user ID into the service methods and reject withErrConversationNotFoundwhen it doesn't match (mirroring howSendGlobalChatMessagevalidatessession.ActorUserID). -
Verify
X-Actor-User-IDbinding — When an agent API key request setsX-Actor-User-ID, the middleware should ensure the agent is the global agent currently chatting with that user (e.g. by checking an activeagent_chat_sessionsrow for(agent_id, actor_user_id)), otherwise a leaked or rogue agent API key can impersonate arbitrary users for global MCP tool calls.
Recommended follow-up
- Consider making
GET /projects/:projectId/agents/:agentIdalso resolve invited global agents (or return a clearer error/message), sinceListAgentsreturns them and the UI may navigate to a detail page from the project picker.
Detailed comments are inline below.
Kimi K2 (free via Pullfrog for OSS) | 𝕏
|
Important I am requesting changes on this PR — the review above was submitted as a comment review, but the issues below are blockers that should be fixed before merging. Critical authorization gap in global-chat conversations Fix: thread the caller's Unverified Please address these before merge, then I'll re-review.
|
- Updated persist_conversation_event to include actor_user_id in the event payload for global conversations. - Added regression tests to ensure actor_user_id is correctly forwarded in global chat scenarios. - Modified ConversationService interface and its implementation to enforce ownership checks based on actor_user_id for global conversations. - Enhanced HTTP handlers to ensure the authenticated user's ID is used for global conversation operations, preventing unauthorized access. - Updated tests to validate the correct behavior of global conversation methods, ensuring proper ownership enforcement and actor attribution.
There was a problem hiding this comment.
Caution
The prior review's required fix for X-Actor-User-ID binding is still missing. Agent-API-key requests can still impersonate arbitrary users for CreateProject and read any global agent's permissions and invited projects via an unverified X-Agent-ID header.
Reviewed changes
Since the prior Pullfrog review, the new commits:
- Added caller-owned checks to every global-chat conversation path (
GetGlobalConversation,StopGlobalConversation,PauseGlobalConversation,GlobalHeartbeat,SendGlobalConversationMessage, and the events gate), so a caller can no longer read or control another user's global conversation by UUID. - Threaded
actor_user_idthrough the AI-agent worker and the durable/realtime event streams, with regression tests covering global-chat ACP dispatch, stream fields, and paused-sandbox teardown. - Tightened the MCP client so
X-Actor-User-IDis only sent alongsideX-Agent-ID. - Added frontend error handling for global-agent deletion and fixed tab visibility for ACP/global agents.
🚨 Unverified X-Agent-ID / X-Actor-User-ID in agent-API-key authentication
parseActorUserIDHeader stores X-Actor-User-ID and setAPIKeyAuthContext stores X-Agent-ID without verifying either value against the database. Because the agent API key is a single static pre-shared key (apikeysvc.WithAgentKey), any caller with that key can set X-Agent-ID to any agent UUID and X-Actor-User-ID to any user UUID.
Downstream handlers then trust these values:
CreateProject(project_handler.go:251) attributes the new project to the arbitraryactor_user_id.GetMyGlobalPermissions/GetMyInvitedProjects(agent_handler.go:475,502) return the permissions and invited projects of the arbitraryagent_id.
The cleanest fix is to verify the pair in the authentication middleware: when X-Agent-ID is present, require the agent to exist (and ideally be global-scope), and when X-Actor-User-ID is also present, require an active agent_chat_sessions row for (agent_id, actor_user_id). Reject the request if either check fails.
Technical details
# Unverified X-Agent-ID / X-Actor-User-ID in agent-API-key auth
## Affected sites
- `services/api/internal/transport/http/middleware/authn.go:230` — `parseActorUserIDHeader` parses `X-Actor-User-ID` without validation.
- `services/api/internal/transport/http/middleware/authn.go:242` — `setAPIKeyAuthContext` stores `X-Agent-ID` without verifying it against `agents.id`.
- `services/api/internal/transport/http/handler/project_handler.go:251` — `CreateProject` uses `AgentActorUserIDFromRequest` to attribute project creation.
- `services/api/internal/transport/http/handler/agent_handler.go:475` — `GetMyGlobalPermissions` uses `AgentIDFromRequest`.
- `services/api/internal/transport/http/handler/agent_handler.go:502` — `GetMyInvitedProjects` uses `AgentIDFromRequest`.
## Required outcome
- An agent-API-key request cannot act on behalf of a user unless the named agent currently has an active global chat session with that user.
- An agent-API-key request cannot enumerate or operate as an arbitrary agent by changing `X-Agent-ID`.
## Suggested approach
In the agent-API-key branch of `applyAuthn`, after `IsAgentKey` returns true:
1. Parse `X-Agent-ID`; if it is not a valid UUID, reject the request.
2. Look up the agent in `agents` and confirm it exists and is global-scope.
3. If `X-Actor-User-ID` is present, query `agent_chat_sessions` for an active row with `agent_id = X-Agent-ID AND actor_user_id = X-Actor-User-ID AND deleted_at IS NULL`; reject if none exists.
4. Only then store both IDs in the context.
This keeps the verification close to the authentication boundary so existing and future handlers automatically inherit it.Kimi K2 (free via Pullfrog for OSS) | 𝕏
- Added `WithAgentIdentityStore` method to `apikeysvc.Service` to wire in agent identity verification. - Introduced `AgentIdentityStore` interface to define methods for agent lookups. - Implemented `FindVisibleAgentInProject` in `AgentRepository` to retrieve agents visible in a specific project. - Enhanced `GetAgent` method in `agent.Service` to utilize the new visibility check. - Updated authentication middleware to verify `X-Agent-ID` and `X-Actor-User-ID` claims against the database. - Added tests to ensure proper handling of agent identity verification, including cases for invalid and unverifiable claims. - Introduced a passthrough implementation of `AgentIdentityStore` for integration tests to bypass actual database checks.
There was a problem hiding this comment.
✅ No new issues found.
The prior review's critical concern about unverified
X-Agent-ID/X-Actor-User-IDheaders in agent-API-key authentication is now fixed and covered by regression tests.
Reviewed changes
Since the prior Pullfrog review, the new commits:
- Implemented agent-API-key identity verification via
middleware.AgentIdentityVerifier:X-Agent-IDis validated againstagents.idandX-Actor-User-IDis validated against an activeagent_chat_sessionsrow for(agent_id, actor_user_id); failed claims are rejected in required-auth routes and dropped inOptionalAuthn. - Wired the verifier into production bootstrap via
apiKeyService.WithAgentIdentityStore(agentRepo)so unverified claims fail closed rather than being trusted by default. - Added regression tests covering malformed
X-Agent-ID, unverifiableX-Agent-ID, project-scoped agent + actor rejection, and global-agent + active session acceptance. - Added
FindVisibleAgentInProjectsoGET /projects/:projectId/agents/:agentIdresolves invited global agents instead of returning 404, addressing the prior recommended follow-up. - Updated authorization middleware to route global-scope, agent-authenticated requests through
HasGlobalPermissionsForAgentrather than the shared bot subject.
All prior Pullfrog review threads have been resolved and the affected Go test packages pass.
Kimi K2 (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior Pullfrog review, the new commit:
- Updated the global agent chat float visibility in
apps/web/src/routes/_authenticated.tsxso it is also hidden on the global Conversations page (/conversationsand nested routes), avoiding a redundant chat entry point alongside the page's ownNewConversationThread.The regex
/^\/conversations(\/|$)/correctly excludes/conversations/*while leaving the float available on all other non-project authenticated routes.
Kimi K2 (free via Pullfrog for OSS) | 𝕏

Summary
Introduces global agents — agents that can be managed independently of any single project — and reworks agent permissions, workspace stats, and the agents/conversations UI to support them.
Key Changes
Global agents & permissions
ProjectHandlernow enforces that eitheruser_idoragent_idmust be provided when adding a member.Actor attribution
build_mcp_configaccepts an optionalactor_user_idso actions performed on a user's behalf are tracked correctly.run_conversationpassesactor_user_idthrough when building MCP config.Workspace stats
CountDistinctAgentsByProjects(member repo/service) andCountOpenTasksByProjects(task repo/service) to aggregate counts across multiple projects in a single query.ProjectHandleruses these instead of per-project queries for workspace statistics.Frontend
agent-card,agent-detail,create-agent-dialog,agent-pickerupdates.conversations-layout,new-conversation-thread, global AI chat float (ai-chat-float-global),use-global-agent-realtimehook.admin/agents,admin/agents/$agentId, top-levelconversationsandconversations/$conversationId.Database & docs
docs/ai-agent/database-schema.mdanddocs/architecture/database-schema.mdupdated to match.i18n
admin.jsonandprojects.jsonstrings added/updated across all locales (en, es, fr, ja, ko, pt-BR, ru, vi, zh-CN).Testing