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:
@@ -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);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user