Skip to content

Multiproject Isolation ​

How Flightdeck isolates agents across projects while keeping the UI fully visible.

TL;DR

Agents can only see and message other agents in the same project. The browser dashboard sees everything. File locks are global (shared filesystem), but their visibility is project-scoped.

Isolation Model ​

LayerAgentsBrowser UI
Commands (AGENT_MESSAGE, DIRECT_MESSAGE, QUERY_CREW, INTERRUPT)Project-scoped — cross-project rejectedN/A
WebSocket eventsFiltered by subscribe-projectFull visibility — init sends all agents/locks
REST API routesFiltered by ?projectId=Full visibility — omit param to see all
File locks, activity log, collective memoryStored with projectId columnQueryable across projects

Key principle: Isolation is for agents. The human user sees everything.

How It Works ​

Agent-to-Agent Commands ​

When an agent sends a command that targets another agent (AGENT_MESSAGE, DIRECT_MESSAGE, INTERRUPT), the system checks whether both agents belong to the same project. If not, the command is rejected with an error:

Agent not found in current project

QUERY_CREW returns only agents from the requesting agent's project — agents in other projects are invisible.

BROADCAST is naturally project-scoped because it sends only to agents under the same lead.

Sub-Agent Inheritance ​

When a lead spawns a sub-agent via CREATE_AGENT, the new agent automatically inherits the lead's projectId. This ensures all agents in a delegation hierarchy belong to the same project without manual assignment.

WebSocket ​

On initial connection, the WebSocket sends all agents, locks, and state to the client. This is intentional — the browser dashboard needs full cross-project visibility.

For agent-side filtering, clients can send a subscribe-project message:

json
{ "type": "subscribe-project", "projectId": "project-123" }

After subscribing, the client only receives events for agents in that project. Unsubscribed clients (like the browser) continue to receive everything.

REST API Routes ​

All data routes support an optional ?projectId= query parameter:

  • GET /api/agents?projectId=... — returns only agents in that project
  • GET /api/coordination/locks?projectId=... — returns only locks from that project
  • GET /api/coordination/summary?projectId=... — scoped coordination summary
  • GET /api/decisions?projectId=... — scoped decision log

When projectId is omitted, all data is returned (backward compatible, UI default).

File Locks ​

File locks are intentionally global for conflict detection. Two agents in different projects editing the same file on a shared filesystem would still conflict. The lock system prevents this.

However, lock visibility is project-scoped: GET /api/coordination/locks?projectId=... returns only locks held by agents in that project.

Supporting Systems ​

These systems store a projectId column for scoping:

SystemColumnPurpose
file_locks tableproject_idTrack which project owns each lock
activity_log tableproject_idFilter activity by project
collective_memory tableproject_idPer-project shared memory

Concurrency limits (getRunningCountByProject()) are per-project, so one project's agent count doesn't block another.

Database Migration ​

Migration 0016_project_isolation_supporting.sql adds project_id TEXT DEFAULT '' to:

  • file_locks
  • activity_log
  • collective_memory

Existing rows get an empty string default, preserving backward compatibility for single-project setups.

Implementation ​

The core methods live in AgentManager:

MethodPurpose
getByProject(projectId)Returns agents belonging to a specific project
getProjectIdForAgent(agentId)Resolves an agent's project membership
getRunningCountByProject(projectId)Per-project concurrency count

getProjectIdForAgent is available on CommandHandlerContext, so all command handlers can check project boundaries.

Documentation generated by AI

Documentation generated by AI