Skip to content

Feature/introduce global agents - #367

Merged
pikann merged 9 commits into
masterfrom
feature/introduce-global-agents
Aug 6, 2026
Merged

Feature/introduce global agents#367
pikann merged 9 commits into
masterfrom
feature/introduce-global-agents

Conversation

@pikann

@pikann pikann commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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

  • Add global agent support end-to-end (domain model, repository, service, API routes) so agents aren't tied to one project.
  • ProjectHandler now enforces that either user_id or agent_id must be provided when adding a member.
  • Authorization middleware checks global agents against their own roles instead of a shared bot subject.
  • New tests validating global agent permission scenarios.

Actor attribution

  • build_mcp_config accepts an optional actor_user_id so actions performed on a user's behalf are tracked correctly.
  • run_conversation passes actor_user_id through when building MCP config.
  • Project creation is attributed to the correct user when an actor user ID is present.
  • Middleware parses and stores the actor user ID from agent-authenticated requests.

Workspace stats

  • Add CountDistinctAgentsByProjects (member repo/service) and CountOpenTasksByProjects (task repo/service) to aggregate counts across multiple projects in a single query.
  • ProjectHandler uses these instead of per-project queries for workspace statistics.

Frontend

  • New agents UI: agent-card, agent-detail, create-agent-dialog, agent-picker updates.
  • New conversations UI: conversations-layout, new-conversation-thread, global AI chat float (ai-chat-float-global), use-global-agent-realtime hook.
  • New routes: admin/agents, admin/agents/$agentId, top-level conversations and conversations/$conversationId.
  • Sidebar and team page updated for global agent management.

Database & docs

  • Schema updated to support global agents (new fields/constraints).
  • docs/ai-agent/database-schema.md and docs/architecture/database-schema.md updated to match.

i18n

  • admin.json and projects.json strings added/updated across all locales (en, es, fr, ja, ko, pt-BR, ru, vi, zh-CN).

Testing

  • Added/updated unit tests across MCP permissions, agent service, authz, repositories (Go), and agent-api (frontend).

pikann added 5 commits August 5, 2026 11:44
- 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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Global conversation ownership checksGetGlobalConversation, StopGlobalConversation, PauseGlobalConversation, GlobalConversationHeartbeat, SendGlobalConversationMessage, and the events endpoint must verify the caller's users.id matches the conversation's actor_user_id. The cleanest fix is to pass the caller's user ID into the service methods and reject with ErrConversationNotFound when it doesn't match (mirroring how SendGlobalChatMessage validates session.ActorUserID).

  2. Verify X-Actor-User-ID binding — When an agent API key request sets X-Actor-User-ID, the middleware should ensure the agent is the global agent currently chatting with that user (e.g. by checking an active agent_chat_sessions row 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/:agentId also resolve invited global agents (or return a clearer error/message), since ListAgents returns them and the UI may navigate to a detail page from the project picker.

Detailed comments are inline below.

Pullfrog  | Fix it ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

@pullfrog

pullfrog Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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
GET /agents/conversations/:id, GET /agents/conversations/:id/events, POST /agents/conversations/:id/stop, POST /agents/conversations/:id/pause, POST /agents/conversations/:id/heartbeat, and POST /agents/conversations/:id/messages only verify that the conversation is global (project_id IS NULL). They do not verify that the caller is the conversation's actor_user_id. This means any authenticated user can read, control, and inject messages into any other user's global chat simply by knowing the UUID.

Fix: thread the caller's users.id into the service methods and reject with ErrConversationNotFound unless conversation.actor_user_id == callerUserID.

Unverified X-Actor-User-ID in agent-API-key auth
middleware/authn.go accepts X-Actor-User-ID from any request authenticated with the agent API key without binding it to an active global chat session. A leaked agent API key could impersonate any user in global MCP tool calls. The middleware should verify an active agent_chat_sessions row exists for (agent_id, actor_user_id) before honoring the header.

Please address these before merge, then I'll re-review.

Pullfrog  | View workflow run | via Pullfrog | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

pikann added 2 commits August 6, 2026 05:48
- 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.
@pikann pikann linked an issue Aug 6, 2026 that may be closed by this pull request

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_id through 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-ID is only sent alongside X-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 arbitrary actor_user_id.
  • GetMyGlobalPermissions / GetMyInvitedProjects (agent_handler.go:475, 502) return the permissions and invited projects of the arbitrary agent_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.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

Comment thread services/api/internal/transport/http/middleware/authn.go
Comment thread services/api/internal/transport/http/handler/project_handler.go
Comment thread services/api/internal/transport/http/handler/agent_handler.go
- 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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

The prior review's critical concern about unverified X-Agent-ID / X-Actor-User-ID headers 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-ID is validated against agents.id and X-Actor-User-ID is validated against an active agent_chat_sessions row for (agent_id, actor_user_id); failed claims are rejected in required-auth routes and dropped in OptionalAuthn.
  • 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, unverifiable X-Agent-ID, project-scoped agent + actor rejection, and global-agent + active session acceptance.
  • Added FindVisibleAgentInProject so GET /projects/:projectId/agents/:agentId resolves invited global agents instead of returning 404, addressing the prior recommended follow-up.
  • Updated authorization middleware to route global-scope, agent-authenticated requests through HasGlobalPermissionsForAgent rather than the shared bot subject.

All prior Pullfrog review threads have been resolved and the affected Go test packages pass.

Pullfrog  | View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.tsx so it is also hidden on the global Conversations page (/conversations and nested routes), avoiding a redundant chat entry point alongside the page's own NewConversationThread.

The regex /^\/conversations(\/|$)/ correctly excludes /conversations/* while leaving the float available on all other non-project authenticated routes.

Pullfrog  | View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

@pikann
pikann merged commit 7ee8746 into master Aug 6, 2026
12 checks passed
@pikann
pikann deleted the feature/introduce-global-agents branch August 6, 2026 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] global agents

1 participant