feat(#93): implement agent spawn via federation

Implements FED-010: Agent Spawn via Federation feature that enables
spawning and managing Claude agents on remote federated Mosaic Stack
instances via COMMAND message type.

Features:
- Federation agent command types (spawn, status, kill)
- FederationAgentService for handling agent operations
- Integration with orchestrator's agent spawner/lifecycle services
- API endpoints for spawning, querying status, and killing agents
- Full command routing through federation COMMAND infrastructure
- Comprehensive test coverage (12/12 tests passing)

Architecture:
- Hub → Spoke: Spawn agents on remote instances
- Command flow: FederationController → FederationAgentService →
  CommandService → Remote Orchestrator
- Response handling: Remote orchestrator returns agent status/results
- Security: Connection validation, signature verification

Files created:
- apps/api/src/federation/types/federation-agent.types.ts
- apps/api/src/federation/federation-agent.service.ts
- apps/api/src/federation/federation-agent.service.spec.ts

Files modified:
- apps/api/src/federation/command.service.ts (agent command routing)
- apps/api/src/federation/federation.controller.ts (agent endpoints)
- apps/api/src/federation/federation.module.ts (service registration)
- apps/orchestrator/src/api/agents/agents.controller.ts (status endpoint)
- apps/orchestrator/src/api/agents/agents.module.ts (lifecycle integration)

Testing:
- 12/12 tests passing for FederationAgentService
- All command service tests passing
- TypeScript compilation successful
- Linting passed

Refs #93

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-03 14:37:06 -06:00
parent a8c8af21e5
commit 12abdfe81d
405 changed files with 13545 additions and 2153 deletions

View File

@@ -58,6 +58,7 @@ All tenant-scoped tables have RLS enabled:
The RLS implementation uses several helper functions:
#### `current_user_id()`
Returns the current user's UUID from the session variable `app.current_user_id`.
```sql
@@ -65,6 +66,7 @@ SELECT current_user_id(); -- Returns UUID or NULL
```
#### `is_workspace_member(workspace_uuid, user_uuid)`
Checks if a user is a member of a workspace.
```sql
@@ -72,6 +74,7 @@ SELECT is_workspace_member('workspace-uuid', 'user-uuid'); -- Returns BOOLEAN
```
#### `is_workspace_admin(workspace_uuid, user_uuid)`
Checks if a user is an owner or admin of a workspace.
```sql
@@ -110,12 +113,9 @@ CREATE POLICY knowledge_links_access ON knowledge_links
Before executing any queries, the API **must** set the current user ID:
```typescript
import { prisma } from '@mosaic/database';
import { prisma } from "@mosaic/database";
async function withUserContext<T>(
userId: string,
fn: () => Promise<T>
): Promise<T> {
async function withUserContext<T>(userId: string, fn: () => Promise<T>): Promise<T> {
await prisma.$executeRaw`SET LOCAL app.current_user_id = ${userId}`;
return fn();
}
@@ -124,7 +124,7 @@ async function withUserContext<T>(
### Example Usage in API Routes
```typescript
import { withUserContext } from '@/lib/db-context';
import { withUserContext } from "@/lib/db-context";
// In a tRPC procedure or API route
export async function getTasks(userId: string, workspaceId: string) {
@@ -167,20 +167,20 @@ For transactions, set the user context within the transaction:
```typescript
await prisma.$transaction(async (tx) => {
await tx.$executeRaw`SET LOCAL app.current_user_id = ${userId}`;
// All queries in this transaction are scoped to the user
const workspace = await tx.workspace.create({
data: { name: 'New Workspace', ownerId: userId },
data: { name: "New Workspace", ownerId: userId },
});
await tx.workspaceMember.create({
data: {
workspaceId: workspace.id,
userId,
role: 'OWNER',
role: "OWNER",
},
});
return workspace;
});
```
@@ -230,21 +230,21 @@ SELECT * FROM tasks WHERE workspace_id = 'my-workspace-uuid';
### Automated Tests
```typescript
import { prisma } from '@mosaic/database';
import { prisma } from "@mosaic/database";
describe("RLS Policies", () => {
it("should prevent cross-workspace access", async () => {
const user1Id = "user-1-uuid";
const user2Id = "user-2-uuid";
const workspace1Id = "workspace-1-uuid";
const workspace2Id = "workspace-2-uuid";
describe('RLS Policies', () => {
it('should prevent cross-workspace access', async () => {
const user1Id = 'user-1-uuid';
const user2Id = 'user-2-uuid';
const workspace1Id = 'workspace-1-uuid';
const workspace2Id = 'workspace-2-uuid';
// Set context as user 1
await prisma.$executeRaw`SET LOCAL app.current_user_id = ${user1Id}`;
// Should only see workspace 1's tasks
const tasks = await prisma.task.findMany();
expect(tasks.every(t => t.workspaceId === workspace1Id)).toBe(true);
expect(tasks.every((t) => t.workspaceId === workspace1Id)).toBe(true);
});
});
```