diff --git a/apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts b/apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts new file mode 100644 index 0000000..7bb1118 --- /dev/null +++ b/apps/gateway/src/__tests__/integration/tess-cross-surface.integration.test.ts @@ -0,0 +1,192 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { InMemoryDurableSessionStore } from '@mosaicstack/agent'; +import { + createDiscordIngressEnvelope, + DiscordPlugin, + type DiscordIngressPayload, +} from '@mosaicstack/discord-plugin'; +import { InteractionController } from '../../agent/interaction.controller.js'; +import { RuntimeProviderService } from '../../agent/runtime-provider-registry.service.js'; +import { TessDurableSessionService } from '../../agent/tess-durable-session.service.js'; +import { ChatGateway } from '../../chat/chat.gateway.js'; +import { CommandAuthorizationService } from '../../commands/command-authorization.service.js'; + +const SERVICE_TOKEN = 'test-discord-service-token'; +const envKeys = [ + 'DISCORD_SERVICE_TOKEN', + 'DISCORD_SERVICE_TENANT_ID', + 'DISCORD_INTERACTION_BINDINGS', + 'DISCORD_ALLOWED_GUILD_IDS', + 'DISCORD_ALLOWED_CHANNEL_IDS', + 'DISCORD_ALLOWED_USER_IDS', + 'MOSAIC_AGENT_NAME', +] as const; +const priorEnv = new Map(); + +function payload(content: string, messageId: string, correlationId: string): DiscordIngressPayload { + return { + content, + messageId, + correlationId, + guildId: 'guild-1', + channelId: 'channel-1', + userId: 'discord-admin-1', + conversationId: 'Nova:discord:channel-1', + }; +} + +function authorization(): CommandAuthorizationService { + const entries = new Map(); + return new CommandAuthorizationService( + { + select: () => ({ + from: () => ({ where: () => ({ limit: async () => [{ role: 'admin' }] }) }), + }), + } as never, + { + get: async (key: string) => entries.get(key) ?? null, + set: async (key: string, value: string) => entries.set(key, value), + del: async (key: string) => Number(entries.delete(key)), + }, + ); +} + +describe('Tess Discord/CLI durable-session integration', () => { + afterEach(() => { + for (const key of envKeys) { + const value = priorEnv.get(key); + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + priorEnv.clear(); + }); + + it('enrolls through the CLI surface then resolves the same durable session from Discord', async () => { + for (const key of envKeys) priorEnv.set(key, process.env[key]); + process.env['MOSAIC_AGENT_NAME'] = 'Nova'; + process.env['DISCORD_SERVICE_TOKEN'] = SERVICE_TOKEN; + process.env['DISCORD_SERVICE_TENANT_ID'] = 'tenant-1'; + process.env['DISCORD_ALLOWED_GUILD_IDS'] = 'guild-1'; + process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-1'; + process.env['DISCORD_ALLOWED_USER_IDS'] = 'discord-admin-1'; + process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([ + { + instanceId: 'Nova', + guildId: 'guild-1', + channelId: 'channel-1', + pairedUsers: { + 'discord-admin-1': { role: 'admin', mosaicUserId: 'mosaic-admin-1' }, + }, + }, + ]); + + const durable = new TessDurableSessionService( + new InMemoryDurableSessionStore() as never, + {} as never, + ); + const enrollmentRuntime = { + listSessions: vi.fn().mockResolvedValue([{ id: 'runtime-1' }]), + }; + const controller = new InteractionController(enrollmentRuntime as never, durable); + await controller.enroll( + 'Nova', + 'Nova:discord:channel-1', + { providerId: 'fleet', runtimeSessionId: 'runtime-1' }, + { id: 'mosaic-admin-1', tenantId: 'tenant-1' }, + 'cli-enrollment-correlation', + ); + + const authz = authorization(); + const terminated = vi.fn().mockResolvedValue(undefined); + const runtime = new RuntimeProviderService( + { + require: () => ({ + capabilities: async () => ({ supported: ['session.terminate'] }), + terminate: terminated, + }), + } as never, + { record: async () => undefined } as never, + { + consume: (approvalId, action) => + authz.consumeRuntimeTerminationApproval(approvalId, action), + }, + ); + const gateway = new ChatGateway( + {} as never, + {} as never, + {} as never, + {} as never, + {} as never, + {} as never, + authz, + runtime, + durable, + ); + const client = { data: { discordService: true }, emit: vi.fn() }; + const plugin = new DiscordPlugin({ + token: 'unused', + gatewayUrl: 'http://unused', + serviceToken: SERVICE_TOKEN, + allowedGuildIds: ['guild-1'], + allowedChannelIds: ['channel-1'], + allowedUserIds: ['discord-admin-1'], + interactionBindings: [ + { + instanceId: 'Nova', + guildId: 'guild-1', + channelId: 'channel-1', + pairedUsers: { + 'discord-admin-1': { role: 'admin', mosaicUserId: 'mosaic-admin-1' }, + }, + }, + ], + }); + const pluginInternals = plugin as unknown as { + client: { user: { id: string } }; + socket: { connected: boolean; emit: ReturnType }; + handleDiscordMessage(message: unknown): void; + }; + const pluginSocket = { connected: true, emit: vi.fn() }; + pluginInternals.client = { user: { id: 'bot-1' } }; + pluginInternals.socket = pluginSocket; + pluginInternals.handleDiscordMessage({ + id: 'approve-1', + guildId: 'guild-1', + channelId: 'channel-1', + author: { id: 'discord-admin-1', bot: false }, + mentions: { has: () => true }, + content: '<@bot-1> /approve', + channel: { parentId: null }, + attachments: new Map(), + }); + + expect(pluginSocket.emit).toHaveBeenCalledWith('discord:approve', expect.any(Object)); + const approvalEnvelope = pluginSocket.emit.mock.calls[0]?.[1]; + await gateway.handleDiscordApproval(client as never, approvalEnvelope); + const approval = client.emit.mock.calls.find( + ([event]) => event === 'discord:approval', + )?.[1] as { + approvalId: string; + success: boolean; + }; + expect(approval.success).toBe(true); + + await gateway.handleDiscordStop( + client as never, + createDiscordIngressEnvelope( + payload(`/stop ${approval.approvalId}`, 'stop-1', 'discord-stop-correlation'), + SERVICE_TOKEN, + ), + ); + + expect(terminated).toHaveBeenCalledWith( + 'runtime-1', + approval.approvalId, + expect.objectContaining({ actorId: 'mosaic-admin-1' }), + ); + expect(client.emit).toHaveBeenCalledWith('discord:stop', { + correlationId: 'discord-stop-correlation', + success: true, + }); + }); +}); diff --git a/apps/gateway/src/agent/__tests__/agent-service-ownership.test.ts b/apps/gateway/src/agent/__tests__/agent-service-ownership.test.ts index b015155..83e0368 100644 --- a/apps/gateway/src/agent/__tests__/agent-service-ownership.test.ts +++ b/apps/gateway/src/agent/__tests__/agent-service-ownership.test.ts @@ -12,18 +12,24 @@ type AgentServiceInternals = { creating: Map>; }; -function makeService(): AgentService { +function makeService(operatorMemory: unknown = null): AgentService { return new AgentService( - {} as never, + { + getDefaultModel: vi.fn(() => null), + getRegistry: vi.fn(() => ({})), + findModel: vi.fn(), + listAvailableModels: vi.fn(() => []), + } as never, {} as never, {} as never, { available: false } as never, {} as never, - {} as never, - {} as never, + { getToolDefinitions: vi.fn(() => []) } as never, + { loadForSession: vi.fn(async () => ({ metaTools: [], promptAdditions: [] })) } as never, null, null, { collect: vi.fn().mockResolvedValue(undefined) } as never, + operatorMemory as never, ); } @@ -120,6 +126,37 @@ describe('AgentService owner/tenant scope enforcement', () => { expect(internals(service).sessions.has(CONVERSATION_ID)).toBe(false); }); + it('derives the operator-memory scope on the createSession production path', async () => { + const plugin = { capture: vi.fn(), search: vi.fn() }; + const service = makeService(plugin); + const buildTools = vi.spyOn(service as never, 'buildToolsForSandbox').mockReturnValue([]); + + // Session construction reaches the real scope derivation before the intentionally incomplete + // Pi test double rejects later in createAgentSession. + await service.createSession(CONVERSATION_ID, OWNER_SCOPE).catch(() => undefined); + + expect(buildTools).toHaveBeenCalledWith(expect.any(String), OWNER_SCOPE.userId, { + tenantId: OWNER_SCOPE.tenantId, + ownerId: OWNER_SCOPE.userId, + sessionId: CONVERSATION_ID, + }); + }); + + it('denies a foreign actor before it can obtain another session operator-memory scope', async () => { + const plugin = { capture: vi.fn(), search: vi.fn() }; + const service = makeService(plugin); + internals(service).sessions.set(CONVERSATION_ID, makeSession()); + const buildTools = vi.spyOn(service as never, 'buildToolsForSandbox'); + + await expect(service.createSession(CONVERSATION_ID, FOREIGN_SCOPE)).rejects.toBeInstanceOf( + ForbiddenException, + ); + + expect(buildTools).not.toHaveBeenCalled(); + expect(plugin.capture).not.toHaveBeenCalled(); + expect(plugin.search).not.toHaveBeenCalled(); + }); + it('checks owner/tenant scope before returning an in-flight session creation', async () => { const service = makeService(); const session = makeSession(); diff --git a/apps/gateway/src/agent/__tests__/runtime-provider-registry.service.test.ts b/apps/gateway/src/agent/__tests__/runtime-provider-registry.service.test.ts index a521b5d..11c57f6 100644 --- a/apps/gateway/src/agent/__tests__/runtime-provider-registry.service.test.ts +++ b/apps/gateway/src/agent/__tests__/runtime-provider-registry.service.test.ts @@ -22,6 +22,8 @@ import { type RuntimeApprovalVerifier, } from '../runtime-provider-registry.service.js'; +process.env['MOSAIC_AGENT_NAME'] ??= 'test-runtime-agent'; + const OWNER_SCOPE: ActorTenantScope = { userId: 'owner-1', tenantId: 'tenant-1' }; const CONTEXT = { actorScope: OWNER_SCOPE, @@ -35,6 +37,7 @@ class RecordingRuntimeProvider implements AgentRuntimeProvider { readonly sentMessages: RuntimeMessage[] = []; terminateCalls = 0; throwAfterSend = false; + throwAuthorization = false; constructor(private readonly supported: RuntimeCapability[]) {} @@ -74,6 +77,9 @@ class RecordingRuntimeProvider implements AgentRuntimeProvider { ): Promise { this.receivedScopes.push(scope); this.sentMessages.push(message); + if (this.throwAuthorization) { + throw Object.assign(new Error('provider authorization denied'), { code: 'forbidden' }); + } if (this.throwAfterSend) { throw new Error('provider acknowledgement failed'); } @@ -243,6 +249,7 @@ describe('RuntimeProviderService security boundary', (): void => { tenantId: OWNER_SCOPE.tenantId, channelId: CONTEXT.channelId, correlationId: CONTEXT.correlationId, + agentName: process.env['MOSAIC_AGENT_NAME'], }); expect(provider.terminateCalls).toBe(1); }); @@ -292,6 +299,23 @@ describe('RuntimeProviderService security boundary', (): void => { }); }); + it('records a provider authorization rejection as denied rather than provider failure', async (): Promise => { + const provider = new RecordingRuntimeProvider(['session.send']); + provider.throwAuthorization = true; + const audit = new RecordingAuditSink(); + const service = makeService(provider, audit); + + await expect( + service.sendMessage( + 'fleet', + 'session-1', + { content: 'hello', idempotencyKey: 'key-1' }, + CONTEXT, + ), + ).rejects.toThrow(/provider authorization denied/); + expect(audit.events.at(-1)).toMatchObject({ outcome: 'denied', errorCode: 'policy_denied' }); + }); + it('persists only metadata-only runtime audit fields', async (): Promise => { let persisted: unknown; const ingest = async (entry: unknown): Promise => { diff --git a/apps/gateway/src/agent/agent.module.ts b/apps/gateway/src/agent/agent.module.ts index bb1c1a4..f563bcc 100644 --- a/apps/gateway/src/agent/agent.module.ts +++ b/apps/gateway/src/agent/agent.module.ts @@ -9,15 +9,19 @@ import { SkillLoaderService } from './skill-loader.service.js'; import { ProvidersController } from './providers.controller.js'; import { SessionsController } from './sessions.controller.js'; import { AgentConfigsController } from './agent-configs.controller.js'; +import { InteractionController } from './interaction.controller.js'; import { RoutingController } from './routing/routing.controller.js'; +import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; +import { TessDurableSessionService } from './tess-durable-session.service.js'; import { CoordModule } from '../coord/coord.module.js'; import { McpClientModule } from '../mcp-client/mcp-client.module.js'; import { SkillsModule } from '../skills/skills.module.js'; import { GCModule } from '../gc/gc.module.js'; import { LogModule } from '../log/log.module.js'; +import { CommandsModule } from '../commands/commands.module.js'; +import { CommandRuntimeApprovalVerifier } from '../commands/runtime-approval-verifier.js'; import { AGENT_RUNTIME_PROVIDER_REGISTRY, - DenyRuntimeApprovalVerifier, RUNTIME_APPROVAL_VERIFIER, RUNTIME_PROVIDER_AUDIT_SINK, RuntimeProviderAuditService, @@ -26,13 +30,15 @@ import { @Global() @Module({ - imports: [CoordModule, McpClientModule, SkillsModule, GCModule, LogModule], + imports: [CoordModule, McpClientModule, SkillsModule, GCModule, LogModule, CommandsModule], providers: [ ProviderService, ProviderCredentialsService, RoutingService, RoutingEngineService, SkillLoaderService, + TessDurableSessionRepository, + TessDurableSessionService, { provide: AGENT_RUNTIME_PROVIDER_REGISTRY, useFactory: (): AgentRuntimeProviderRegistry => new AgentRuntimeProviderRegistry(), @@ -42,15 +48,20 @@ import { provide: RUNTIME_PROVIDER_AUDIT_SINK, useExisting: RuntimeProviderAuditService, }, - DenyRuntimeApprovalVerifier, { provide: RUNTIME_APPROVAL_VERIFIER, - useExisting: DenyRuntimeApprovalVerifier, + useExisting: CommandRuntimeApprovalVerifier, }, RuntimeProviderService, AgentService, ], - controllers: [ProvidersController, SessionsController, AgentConfigsController, RoutingController], + controllers: [ + ProvidersController, + SessionsController, + AgentConfigsController, + InteractionController, + RoutingController, + ], exports: [ AgentService, ProviderService, @@ -58,6 +69,7 @@ import { RoutingService, RoutingEngineService, SkillLoaderService, + TessDurableSessionService, RuntimeProviderService, AGENT_RUNTIME_PROVIDER_REGISTRY, ], diff --git a/apps/gateway/src/agent/agent.service.ts b/apps/gateway/src/agent/agent.service.ts index f5b2347..4ac42d5 100644 --- a/apps/gateway/src/agent/agent.service.ts +++ b/apps/gateway/src/agent/agent.service.ts @@ -15,9 +15,10 @@ import { type ToolDefinition, } from '@mariozechner/pi-coding-agent'; import type { Brain } from '@mosaicstack/brain'; -import type { Memory } from '@mosaicstack/memory'; +import type { Memory, OperatorMemoryPlugin } from '@mosaicstack/memory'; import { BRAIN } from '../brain/brain.tokens.js'; import { MEMORY } from '../memory/memory.tokens.js'; +import { OPERATOR_MEMORY_PLUGIN } from '../memory/memory.module.js'; import { EmbeddingService } from '../memory/embedding.service.js'; import { CoordService } from '../coord/coord.service.js'; import { ProviderService } from './provider.service.js'; @@ -135,6 +136,9 @@ export class AgentService implements OnModuleDestroy { @Inject(PreferencesService) private readonly preferencesService: PreferencesService | null, @Inject(SessionGCService) private readonly gc: SessionGCService, + @Optional() + @Inject(OPERATOR_MEMORY_PLUGIN) + private readonly operatorMemory: OperatorMemoryPlugin | null = null, ) {} /** @@ -146,6 +150,7 @@ export class AgentService implements OnModuleDestroy { private buildToolsForSandbox( sandboxDir: string, sessionUserId: string | undefined, + sessionScope?: { tenantId: string; ownerId: string; sessionId: string }, ): ToolDefinition[] { return [ ...createBrainTools(this.brain), @@ -154,6 +159,9 @@ export class AgentService implements OnModuleDestroy { this.memory, this.embeddingService.available ? this.embeddingService : null, sessionUserId, + this.operatorMemory && sessionScope + ? { plugin: this.operatorMemory, scope: sessionScope } + : undefined, ), ...createFileTools(sandboxDir), ...createGitTools(sandboxDir), @@ -228,6 +236,7 @@ export class AgentService implements OnModuleDestroy { isAdmin: options.isAdmin, agentConfigId: options.agentConfigId, userId: options.userId, + tenantId: options.tenantId, conversationHistory: options.conversationHistory, }; this.logger.log( @@ -267,7 +276,15 @@ export class AgentService implements OnModuleDestroy { } // Build per-session tools scoped to the sandbox directory and authenticated user - const sandboxTools = this.buildToolsForSandbox(sandboxDir, mergedOptions?.userId); + const sessionUserId = mergedOptions?.userId; + const sessionTenantId = this.tenantIdFor(sessionUserId, mergedOptions?.tenantId); + const sandboxTools = this.buildToolsForSandbox( + sandboxDir, + sessionUserId, + sessionUserId && sessionTenantId + ? { tenantId: sessionTenantId, ownerId: sessionUserId, sessionId } + : undefined, + ); // Combine static tools with dynamically discovered MCP client tools and skill tools const mcpTools = this.mcpClientService.getToolDefinitions(); @@ -362,7 +379,7 @@ export class AgentService implements OnModuleDestroy { sandboxDir, allowedTools, userId: mergedOptions?.userId, - tenantId: this.tenantIdFor(mergedOptions?.userId, mergedOptions?.tenantId), + tenantId: sessionTenantId, agentConfigId: mergedOptions?.agentConfigId, agentName: resolvedAgentName, metrics: { diff --git a/apps/gateway/src/agent/interaction.controller.test.ts b/apps/gateway/src/agent/interaction.controller.test.ts new file mode 100644 index 0000000..db0dad4 --- /dev/null +++ b/apps/gateway/src/agent/interaction.controller.test.ts @@ -0,0 +1,233 @@ +import { firstValueFrom } from 'rxjs'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { RuntimeApprovalDeniedError } from './runtime-provider-registry.service.js'; +import { RuntimeApprovalDeniedFilter } from './runtime-approval-denied.filter.js'; +import { InteractionController } from './interaction.controller.js'; + +describe('InteractionController', (): void => { + afterEach(() => vi.restoreAllMocks()); + + it('maps a denied runtime approval to Fastify HTTP 403', () => { + const send = vi.fn(); + const status = vi.fn().mockReturnValue({ send }); + const response = { status }; + const host = { switchToHttp: () => ({ getResponse: () => response }) }; + + new RuntimeApprovalDeniedFilter().catch(new RuntimeApprovalDeniedError(), host as never); + + expect(status).toHaveBeenCalledWith(403); + expect(send).toHaveBeenCalledWith({ + statusCode: 403, + message: 'Runtime termination approval denied', + }); + }); + + it('honors a differently named configured instance without a code change', async () => { + const prior = process.env['MOSAIC_AGENT_NAME']; + process.env['MOSAIC_AGENT_NAME'] = 'Nova'; + const runtime = { listSessions: vi.fn().mockResolvedValue([]) }; + const controller = new InteractionController(runtime as never, {} as never); + + await expect( + controller.sessions('Nova', 'fleet', { id: 'owner', tenantId: 'team' }, 'corr-1'), + ).resolves.toEqual([]); + await expect( + controller.sessions('Other', 'fleet', { id: 'owner', tenantId: 'team' }, 'corr-1'), + ).rejects.toThrow('Interaction agent is not configured'); + + if (prior === undefined) delete process.env['MOSAIC_AGENT_NAME']; + else process.env['MOSAIC_AGENT_NAME'] = prior; + }); + + it('rejects a request without the non-simple correlation header', async () => { + process.env['MOSAIC_AGENT_NAME'] = 'Nova'; + const controller = new InteractionController({ listSessions: vi.fn() } as never, {} as never); + + await expect(controller.sessions('Nova', 'fleet', { id: 'owner' })).rejects.toThrow( + 'X-Correlation-Id is required', + ); + }); + + it('enrolls a visible runtime session under the cross-surface conversation handle', async () => { + process.env['MOSAIC_AGENT_NAME'] = 'Nova'; + const runtime = { + listSessions: vi.fn().mockResolvedValue([{ id: 'runtime-1' }]), + }; + const durable = { enroll: vi.fn().mockResolvedValue(undefined) }; + const controller = new InteractionController(runtime as never, durable as never); + + await expect( + controller.enroll( + 'Nova', + 'conversation-1', + { providerId: 'fleet', runtimeSessionId: 'runtime-1' }, + { id: 'owner', tenantId: 'team' }, + 'corr-1', + ), + ).resolves.toEqual({ status: 'enrolled', sessionId: 'conversation-1' }); + expect(durable.enroll).toHaveBeenCalledWith( + { + agentName: 'Nova', + sessionId: 'conversation-1', + tenantId: 'team', + ownerId: 'owner', + providerId: 'fleet', + runtimeSessionId: 'runtime-1', + }, + expect.objectContaining({ correlationId: 'corr-1' }), + ); + }); + + it('rejects an invalid attach mode before invoking a provider', async () => { + process.env['MOSAIC_AGENT_NAME'] = 'Nova'; + const controller = new InteractionController({ attach: vi.fn() } as never, {} as never); + + await expect( + controller.attach('Nova', 'durable-1', { mode: 'write' as never }, { id: 'owner' }, 'corr-1'), + ).rejects.toThrow('Interaction attach mode is invalid'); + }); + + it('resumes a durable session by attaching and streaming its runtime events', async () => { + process.env['MOSAIC_AGENT_NAME'] = 'Nova'; + const runtimeEvent = { + type: 'message.delta' as const, + sessionId: 'runtime-1', + cursor: 'cursor-1', + occurredAt: '2026-07-13T00:00:00.000Z', + content: 'resumed', + }; + const runtime = { + attach: vi.fn().mockResolvedValue({ attachmentId: 'attach-1', sessionId: 'runtime-1' }), + streamSession: vi.fn(async function* () { + yield runtimeEvent; + }), + }; + const durable = { + getSnapshot: vi.fn().mockResolvedValue({ + identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, + }), + }; + const controller = new InteractionController(runtime as never, durable as never); + + await controller.attach('Nova', 'conversation-1', { mode: 'read' }, { id: 'owner' }, 'corr-1'); + await expect( + firstValueFrom( + controller.stream('Nova', 'conversation-1', undefined, { id: 'owner' }, 'corr-1'), + ), + ).resolves.toEqual({ data: runtimeEvent }); + + expect(runtime.attach).toHaveBeenCalledWith( + 'fleet', + 'runtime-1', + 'read', + expect.objectContaining({ correlationId: 'corr-1' }), + ); + expect(runtime.streamSession).toHaveBeenCalledWith( + 'fleet', + 'runtime-1', + undefined, + expect.objectContaining({ correlationId: 'corr-1' }), + ); + }); + + it('does not create a runtime stream after the SSE subscriber disconnects during snapshot lookup', async () => { + process.env['MOSAIC_AGENT_NAME'] = 'Nova'; + let resolveSnapshot!: (value: { identity: Record }) => void; + const snapshot = new Promise<{ identity: Record }>((resolve) => { + resolveSnapshot = resolve; + }); + const runtime = { streamSession: vi.fn() }; + const durable = { getSnapshot: vi.fn().mockReturnValue(snapshot) }; + const controller = new InteractionController(runtime as never, durable as never); + + const subscription = controller + .stream('Nova', 'conversation-1', undefined, { id: 'owner' }, 'corr-1') + .subscribe(); + subscription.unsubscribe(); + resolveSnapshot({ + identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(runtime.streamSession).not.toHaveBeenCalled(); + }); + + it.each([ + ['wrong actor', { getSnapshot: vi.fn().mockRejectedValue(new Error('scope mismatch')) }], + [ + 'session-agent mismatch', + { + getSnapshot: vi.fn().mockResolvedValue({ + identity: { agentName: 'Other', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, + }), + }, + ], + ])('denies a CLI stop for %s', async (_reason, durable) => { + process.env['MOSAIC_AGENT_NAME'] = 'Nova'; + const runtime = { terminate: vi.fn().mockResolvedValue(undefined) }; + const controller = new InteractionController(runtime as never, durable as never); + + await expect( + controller.stop( + 'Nova', + 'durable-1', + { approvalRef: 'approval-1' }, + { id: 'owner' }, + 'corr-1', + ), + ).rejects.toBeDefined(); + expect(runtime.terminate).not.toHaveBeenCalled(); + }); + + it('surfaces a denied runtime approval to the CLI interaction surface', async () => { + process.env['MOSAIC_AGENT_NAME'] = 'Nova'; + const runtime = { + terminate: vi.fn().mockRejectedValue(new Error('Runtime termination approval denied')), + }; + const durable = { + getSnapshot: vi.fn().mockResolvedValue({ + identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, + }), + }; + const controller = new InteractionController(runtime as never, durable as never); + + await expect( + controller.stop( + 'Nova', + 'durable-1', + { approvalRef: 'approval-1' }, + { id: 'owner' }, + 'corr-1', + ), + ).rejects.toThrow('Runtime termination approval denied'); + }); + + it('uses the durable session identity and runtime registry for an approved stop', async () => { + process.env['MOSAIC_AGENT_NAME'] = 'Nova'; + const runtime = { terminate: vi.fn().mockResolvedValue(undefined) }; + const durable = { + getSnapshot: vi.fn().mockResolvedValue({ + identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, + }), + }; + const controller = new InteractionController(runtime as never, durable as never); + + await controller.stop( + 'Nova', + 'durable-1', + { approvalRef: 'approval-1' }, + { id: 'owner' }, + 'corr-1', + ); + + expect(runtime.terminate).toHaveBeenCalledWith( + 'fleet', + 'runtime-1', + 'approval-1', + expect.objectContaining({ + correlationId: 'corr-1', + actorScope: { userId: 'owner', tenantId: 'owner' }, + }), + ); + }); +}); diff --git a/apps/gateway/src/agent/interaction.controller.ts b/apps/gateway/src/agent/interaction.controller.ts new file mode 100644 index 0000000..cc247fd --- /dev/null +++ b/apps/gateway/src/agent/interaction.controller.ts @@ -0,0 +1,279 @@ +import { + Body, + Controller, + ForbiddenException, + Get, + Headers, + Sse, + Inject, + Param, + Post, + Query, + UseGuards, + UseFilters, +} from '@nestjs/common'; +import type { RuntimeAttachMode, RuntimeStreamEvent } from '@mosaicstack/types'; +import { Observable } from 'rxjs'; +import { AuthGuard } from '../auth/auth.guard.js'; +import { CurrentUser } from '../auth/current-user.decorator.js'; +import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js'; +import { TessDurableSessionService } from './tess-durable-session.service.js'; +import { RuntimeApprovalDeniedFilter } from './runtime-approval-denied.filter.js'; +import { + RuntimeProviderService, + type RuntimeProviderRequestContext, +} from './runtime-provider-registry.service.js'; + +/** + * Authenticated HTTP boundary for operator interaction clients. Identity is + * selected from deployment configuration, never a client-side command name. + */ +@Controller('api/interaction/:agentName') +@UseGuards(AuthGuard) +@UseFilters(RuntimeApprovalDeniedFilter) +export class InteractionController { + constructor( + @Inject(RuntimeProviderService) private readonly runtime: RuntimeProviderService, + @Inject(TessDurableSessionService) private readonly durable: TessDurableSessionService, + ) {} + + @Get('sessions') + async sessions( + @Param('agentName') agentName: string, + @Query('provider') providerId: string, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ) { + this.assertConfiguredAgent(agentName); + return this.runtime.listSessions( + this.requiredProvider(providerId), + this.context(user, correlationId), + ); + } + + @Get('tree') + async tree( + @Param('agentName') agentName: string, + @Query('provider') providerId: string, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ) { + this.assertConfiguredAgent(agentName); + return this.runtime.getSessionTree( + this.requiredProvider(providerId), + this.context(user, correlationId), + ); + } + + /** + * Bind an existing, authorized runtime session to the stable conversation ID. + * This is the lifecycle boundary where both runtime identifiers are known. + */ + @Post('sessions/:sessionId/enroll') + async enroll( + @Param('agentName') agentName: string, + @Param('sessionId') sessionId: string, + @Body() body: { providerId?: string; runtimeSessionId?: string } = {}, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ) { + this.assertConfiguredAgent(agentName); + const providerId = this.requiredProvider(body.providerId ?? ''); + const runtimeSessionId = body.runtimeSessionId?.trim(); + if (!runtimeSessionId) throw new ForbiddenException('Runtime session identity is required'); + const context = this.context(user, correlationId); + const sessions = await this.runtime.listSessions(providerId, context); + if (!sessions.some((session): boolean => session.id === runtimeSessionId)) { + throw new ForbiddenException('Runtime session is not visible to this actor'); + } + await this.durable.enroll( + { + agentName, + sessionId, + tenantId: context.actorScope.tenantId, + ownerId: context.actorScope.userId, + providerId, + runtimeSessionId, + }, + context, + ); + return { status: 'enrolled', sessionId }; + } + + @Post('sessions/:sessionId/attach') + async attach( + @Param('agentName') agentName: string, + @Param('sessionId') sessionId: string, + @Body() body: { mode?: RuntimeAttachMode } = {}, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ) { + this.assertConfiguredAgent(agentName); + const context = this.context(user, correlationId); + const mode = body.mode ?? 'read'; + if (mode !== 'read' && mode !== 'control') { + throw new ForbiddenException('Interaction attach mode is invalid'); + } + const snapshot = await this.durable.getSnapshot(sessionId, context); + this.assertSessionAgent(snapshot.identity.agentName, agentName); + return this.runtime.attach( + snapshot.identity.providerId, + snapshot.identity.runtimeSessionId, + mode, + context, + ); + } + + @Sse('sessions/:sessionId/stream') + stream( + @Param('agentName') agentName: string, + @Param('sessionId') sessionId: string, + @Query('cursor') cursor: string | undefined, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ): Observable<{ data: RuntimeStreamEvent }> { + this.assertConfiguredAgent(agentName); + const context = this.context(user, correlationId); + return new Observable((subscriber) => { + let iterator: AsyncIterator | undefined; + let cancelled = false; + void (async (): Promise => { + try { + const snapshot = await this.durable.getSnapshot(sessionId, context); + if (cancelled || subscriber.closed) return; + this.assertSessionAgent(snapshot.identity.agentName, agentName); + iterator = this.runtime + .streamSession( + snapshot.identity.providerId, + snapshot.identity.runtimeSessionId, + cursor?.trim() || undefined, + context, + ) + [Symbol.asyncIterator](); + if (cancelled || subscriber.closed) { + await iterator.return?.(); + return; + } + while (!cancelled && !subscriber.closed) { + const next = await iterator.next(); + if (next.done || cancelled || subscriber.closed) break; + subscriber.next({ data: next.value }); + } + if (!subscriber.closed) subscriber.complete(); + } catch (error: unknown) { + if (!subscriber.closed) subscriber.error(error); + } + })(); + return (): void => { + cancelled = true; + void iterator?.return?.(); + }; + }); + } + + @Post('sessions/:sessionId/send') + async send( + @Param('agentName') agentName: string, + @Param('sessionId') sessionId: string, + @Body() body: { content?: string; idempotencyKey?: string } = {}, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ) { + this.assertConfiguredAgent(agentName); + if (!body.content?.trim() || !body.idempotencyKey?.trim()) { + throw new ForbiddenException('Content and idempotency key are required'); + } + const context = this.context(user, correlationId); + const snapshot = await this.durable.getSnapshot(sessionId, context); + this.assertSessionAgent(snapshot.identity.agentName, agentName); + const input = { + sessionId, + content: body.content, + idempotencyKey: body.idempotencyKey, + correlationId: context.correlationId, + context, + }; + await this.durable.queueProviderSend(input); + await this.durable.dispatchProviderOutbox(sessionId, input); + return { status: 'queued', sessionId }; + } + + @Post('sessions/:sessionId/stop') + async stop( + @Param('agentName') agentName: string, + @Param('sessionId') sessionId: string, + @Body() body: { approvalRef?: string } = {}, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ) { + this.assertConfiguredAgent(agentName); + if (!body.approvalRef?.trim()) + throw new ForbiddenException('Exact-action approval is required'); + const context = this.context(user, correlationId); + const snapshot = await this.durable.getSnapshot(sessionId, context); + this.assertSessionAgent(snapshot.identity.agentName, agentName); + await this.runtime.terminate( + snapshot.identity.providerId, + snapshot.identity.runtimeSessionId, + body.approvalRef, + context, + ); + return { status: 'stopped', sessionId }; + } + + @Post('sessions/:sessionId/recover') + async recover( + @Param('agentName') agentName: string, + @Param('sessionId') sessionId: string, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ) { + this.assertConfiguredAgent(agentName); + const context = this.context(user, correlationId); + const snapshot = await this.durable.getSnapshot(sessionId, context); + this.assertSessionAgent(snapshot.identity.agentName, agentName); + await this.durable.recoverProviderSession(sessionId, { + sessionId, + content: '', + idempotencyKey: `recovery:${context.correlationId}`, + correlationId: context.correlationId, + context, + }); + return { status: 'recovered', sessionId }; + } + + private context( + user: AuthenticatedUserLike, + correlationId?: string, + ): RuntimeProviderRequestContext { + const requestCorrelationId = correlationId?.trim(); + // This non-simple request header is mandatory for mutations. Browser + // cross-origin requests cannot set it without a CORS preflight, and the + // gateway's allowlist rejects untrusted origins before the handler runs. + if (!requestCorrelationId) { + throw new ForbiddenException('X-Correlation-Id is required'); + } + return { + actorScope: scopeFromUser(user), + channelId: 'cli', + correlationId: requestCorrelationId, + }; + } + + private assertConfiguredAgent(agentName: string): void { + const configured = process.env['MOSAIC_AGENT_NAME']?.trim(); + if (!configured || configured !== agentName) { + throw new ForbiddenException('Interaction agent is not configured for this request'); + } + } + + private assertSessionAgent(sessionAgentName: string, agentName: string): void { + if (sessionAgentName !== agentName) + throw new ForbiddenException('Interaction session identity mismatch'); + } + + private requiredProvider(providerId: string): string { + if (!providerId?.trim()) throw new ForbiddenException('Runtime provider is required'); + return providerId; + } +} diff --git a/apps/gateway/src/agent/runtime-approval-denied.filter.ts b/apps/gateway/src/agent/runtime-approval-denied.filter.ts new file mode 100644 index 0000000..22aae38 --- /dev/null +++ b/apps/gateway/src/agent/runtime-approval-denied.filter.ts @@ -0,0 +1,13 @@ +import { Catch, type ArgumentsHost, type ExceptionFilter } from '@nestjs/common'; +import { RuntimeApprovalDeniedError } from './runtime-provider-registry.service.js'; + +/** Maps a consumed/missing runtime approval to a stable HTTP authorization response. */ +@Catch(RuntimeApprovalDeniedError) +export class RuntimeApprovalDeniedFilter implements ExceptionFilter { + catch(_exception: RuntimeApprovalDeniedError, host: ArgumentsHost): void { + const response = host.switchToHttp().getResponse<{ + status(code: number): { send(body: { statusCode: number; message: string }): void }; + }>(); + response.status(403).send({ statusCode: 403, message: 'Runtime termination approval denied' }); + } +} diff --git a/apps/gateway/src/agent/runtime-provider-registry.service.ts b/apps/gateway/src/agent/runtime-provider-registry.service.ts index c56906f..b824bb0 100644 --- a/apps/gateway/src/agent/runtime-provider-registry.service.ts +++ b/apps/gateway/src/agent/runtime-provider-registry.service.ts @@ -64,13 +64,20 @@ export interface RuntimeTerminationAction { tenantId: string; channelId: string; correlationId: string; + agentName: string; } export interface RuntimeApprovalVerifier { consume(approvalRef: string, action: RuntimeTerminationAction): Promise; } -class RuntimeApprovalDeniedError extends Error { +function configuredAgentName(): string { + const agentName = process.env['MOSAIC_AGENT_NAME']?.trim(); + if (!agentName) throw new RuntimeApprovalDeniedError(); + return agentName; +} + +export class RuntimeApprovalDeniedError extends Error { constructor() { super('Runtime termination approval denied'); } @@ -261,6 +268,7 @@ export class RuntimeProviderService { tenantId: scope.tenantId, channelId: scope.channelId, correlationId: scope.correlationId, + agentName: configuredAgentName(), }); if (!approved) { throw new RuntimeApprovalDeniedError(); @@ -293,7 +301,7 @@ export class RuntimeProviderService { return result; } catch (error: unknown) { const durationMs = Date.now() - startedAt; - if (invocationStarted && !(error instanceof RuntimeApprovalDeniedError)) { + if (invocationStarted && !this.isAuthorizationDenied(error)) { await this.recordFailure(providerId, operation, scope, resourceId, durationMs); } else { await this.record( @@ -352,6 +360,17 @@ export class RuntimeProviderService { } } + private isAuthorizationDenied(error: unknown): boolean { + return ( + error instanceof RuntimeApprovalDeniedError || + error instanceof ForbiddenException || + (typeof error === 'object' && + error !== null && + 'code' in error && + (error as { code?: unknown }).code === 'forbidden') + ); + } + private provider(providerId: string): AgentRuntimeProvider { try { return this.registry.require(providerId); diff --git a/apps/gateway/src/agent/tess-durable-session.dto.ts b/apps/gateway/src/agent/tess-durable-session.dto.ts new file mode 100644 index 0000000..9248797 --- /dev/null +++ b/apps/gateway/src/agent/tess-durable-session.dto.ts @@ -0,0 +1,10 @@ +import type { RuntimeProviderRequestContext } from './runtime-provider-registry.service.js'; + +/** Server-side request for a replay-safe provider message. */ +export interface TessProviderOutboxDto { + sessionId: string; + idempotencyKey: string; + correlationId: string; + content: string; + context: RuntimeProviderRequestContext; +} diff --git a/apps/gateway/src/agent/tess-durable-session.repository.test.ts b/apps/gateway/src/agent/tess-durable-session.repository.test.ts new file mode 100644 index 0000000..c194499 --- /dev/null +++ b/apps/gateway/src/agent/tess-durable-session.repository.test.ts @@ -0,0 +1,418 @@ +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { createHash } from 'node:crypto'; +import { eq, sql, interactionCheckpoints, interactionInbox } from '@mosaicstack/db'; +import { DurableSessionCoordinator, type DurableSessionIdentity } from '@mosaicstack/agent'; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createPgliteDb, runPgliteMigrations, type DbHandle } from '@mosaicstack/db'; +import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; +import { TessDurableSessionService } from './tess-durable-session.service.js'; + +const IDENTITY: DurableSessionIdentity = { + agentName: 'Nova', + sessionId: 'tess-pglite-session', + tenantId: 'tenant-pglite', + ownerId: 'tess-owner', + providerId: 'fleet', + runtimeSessionId: 'nova', +}; + +describe('TessDurableSessionRepository', () => { + let dataDir: string | undefined; + let handle: DbHandle; + let previousAuthSecret: string | undefined; + + beforeAll(async (): Promise => { + previousAuthSecret = process.env['BETTER_AUTH_SECRET']; + process.env['BETTER_AUTH_SECRET'] = 'tess-durable-state-test-sealing-key'; + dataDir = mkdtempSync(join(tmpdir(), 'tess-durable-state-')); + handle = createPgliteDb(dataDir); + await runPgliteMigrations(handle); + await seedOwner(handle); + }, 30_000); + + beforeEach(async (): Promise => { + await handle.db.execute(sql`DELETE FROM interaction_handoffs`); + await handle.db.execute(sql`DELETE FROM interaction_checkpoints`); + await handle.db.execute(sql`DELETE FROM interaction_inbox`); + await handle.db.execute(sql`DELETE FROM interaction_outbox`); + await handle.db.execute(sql`DELETE FROM interaction_sessions`); + }); + + afterAll(async (): Promise => { + await handle.close(); + if (dataDir) rmSync(dataDir, { recursive: true, force: true }); + if (previousAuthSecret === undefined) delete process.env['BETTER_AUTH_SECRET']; + else process.env['BETTER_AUTH_SECRET'] = previousAuthSecret; + }); + + it('survives a full PGlite close/reopen mid-session without duplicate inbox or outbox side effects', async () => { + const beforeRestart = new DurableSessionCoordinator( + new TessDurableSessionRepository(handle.db), + ); + await beforeRestart.create(IDENTITY); + await beforeRestart.receive({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'inbox-before-kill', + correlationId: 'correlation-before-kill', + content: 'resume after a kill', + }); + await beforeRestart.enqueueOutbox({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'outbox-before-kill', + correlationId: 'correlation-before-kill', + channelId: 'cli', + kind: 'provider.send', + content: 'one response only', + }); + await beforeRestart.checkpoint({ + sessionId: IDENTITY.sessionId, + checkpointId: 'checkpoint-before-kill', + cursor: 'cursor-before-kill', + summary: 'restart-safe state', + compactionEpoch: 1, + }); + await beforeRestart.handoff({ + sessionId: IDENTITY.sessionId, + handoffId: 'handoff-before-kill', + destination: 'mos', + correlationId: 'correlation-before-kill', + checkpointId: 'checkpoint-before-kill', + status: 'pending', + }); + await beforeRestart.checkpoint({ + sessionId: IDENTITY.sessionId, + checkpointId: 'checkpoint-after-handoff', + cursor: 'cursor-after-handoff', + summary: 'newer state cannot strand the portable handoff', + compactionEpoch: 2, + }); + + await handle.close(); + handle = createPgliteDb(dataDir!); + + const afterRestart = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); + const recovered = await afterRestart.recover(IDENTITY.sessionId); + const resumedHandoff = await afterRestart.resumeHandoff('handoff-before-kill'); + const handled: string[] = []; + const effects: string[] = []; + + await afterRestart.drainInbox(IDENTITY.sessionId, async (entry): Promise => { + handled.push(entry.idempotencyKey); + }); + await afterRestart.dispatchOutbox(IDENTITY.sessionId, async (entry): Promise => { + effects.push(entry.idempotencyKey); + }); + await afterRestart.drainInbox(IDENTITY.sessionId, async (entry): Promise => { + handled.push(entry.idempotencyKey); + }); + await afterRestart.dispatchOutbox(IDENTITY.sessionId, async (entry): Promise => { + effects.push(entry.idempotencyKey); + }); + + expect(recovered.identity).toEqual(IDENTITY); + expect(recovered.checkpoint).toMatchObject({ checkpointId: 'checkpoint-after-handoff' }); + expect(recovered.handoffs).toMatchObject([{ handoffId: 'handoff-before-kill' }]); + expect(resumedHandoff.checkpoint).toMatchObject({ checkpointId: 'checkpoint-before-kill' }); + expect(handled).toEqual(['inbox-before-kill']); + expect(effects).toEqual(['outbox-before-kill']); + }, 30_000); + + it('redacts sensitive durable payloads before persistence', async () => { + const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); + await coordinator.create(IDENTITY); + await coordinator.receive({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'redacted-inbox', + correlationId: 'correlation-redaction', + content: 'api_key=super-secret-canary', + }); + await coordinator.enqueueOutbox({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'redacted-outbox', + correlationId: 'correlation-redaction', + channelId: 'cli', + kind: 'provider.send', + content: 'email operator@example.test api_key=super-secret-canary', + }); + await coordinator.checkpoint({ + sessionId: IDENTITY.sessionId, + checkpointId: 'redacted-checkpoint', + cursor: 'bearer super-secret-canary', + summary: 'email operator@example.test', + compactionEpoch: 0, + }); + + const snapshot = await coordinator.snapshot(IDENTITY.sessionId); + const [persisted] = await handle.db + .select({ content: interactionInbox.content }) + .from(interactionInbox) + .where(eq(interactionInbox.idempotencyKey, 'redacted-inbox')); + + expect(JSON.stringify(snapshot)).not.toContain('super-secret-canary'); + expect(JSON.stringify(snapshot)).not.toContain('operator@example.test'); + expect(persisted?.content).not.toContain('super-secret-canary'); + expect(persisted?.content).not.toContain('[REDACTED]'); + }, 30_000); + + it('fails closed when the configured idempotency secret is unavailable', async () => { + const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); + await coordinator.create(IDENTITY); + const secret = process.env['BETTER_AUTH_SECRET']; + delete process.env['BETTER_AUTH_SECRET']; + try { + await expect( + coordinator.receive({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'requires-idempotency-secret', + correlationId: 'correlation-secret', + content: 'sensitive payload', + }), + ).rejects.toThrow(/required for durable idempotency digests/); + } finally { + if (secret === undefined) delete process.env['BETTER_AUTH_SECRET']; + else process.env['BETTER_AUTH_SECRET'] = secret; + } + }, 30_000); + + it('uses keyed pre-redaction digests to reject distinct sensitive checkpoint payloads', async () => { + const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); + await coordinator.create(IDENTITY); + const input = { + sessionId: IDENTITY.sessionId, + checkpointId: 'checkpoint-secret-conflict', + cursor: 'api_key=secret-one', + summary: 'bearer secret-one', + compactionEpoch: 1, + }; + await coordinator.checkpoint(input); + + await expect( + coordinator.checkpoint({ + ...input, + cursor: 'api_key=secret-two', + summary: 'bearer secret-two', + }), + ).rejects.toThrow(/checkpoint identity conflict/); + + const [persisted] = await handle.db + .select({ + digest: interactionCheckpoints.contentDigest, + cursor: interactionCheckpoints.cursor, + }) + .from(interactionCheckpoints) + .where(eq(interactionCheckpoints.checkpointId, input.checkpointId)); + expect(persisted?.cursor).not.toContain('secret-one'); + expect(persisted?.digest).not.toBe( + createHash('sha256') + .update(JSON.stringify([input.cursor, input.summary])) + .digest('hex'), + ); + + await coordinator.checkpoint({ + ...input, + checkpointId: 'checkpoint-delimiter-conflict', + cursor: 'a\u0000b', + summary: 'c', + }); + await expect( + coordinator.checkpoint({ + ...input, + checkpointId: 'checkpoint-delimiter-conflict', + cursor: 'a', + summary: 'b\u0000c', + }), + ).rejects.toThrow(/checkpoint identity conflict/); + }, 30_000); + + it('rejects distinct sensitive inbox and outbox payloads under reused idempotency keys', async () => { + const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); + await coordinator.create(IDENTITY); + const inbox = { + sessionId: IDENTITY.sessionId, + idempotencyKey: 'inbox-secret-conflict', + correlationId: 'correlation-inbox-secret', + content: 'api_key=secret-one', + }; + const outbox = { + sessionId: IDENTITY.sessionId, + idempotencyKey: 'outbox-secret-conflict', + correlationId: 'correlation-outbox-secret', + channelId: 'cli', + kind: 'provider.send', + content: 'api_key=secret-one', + }; + await coordinator.receive(inbox); + await coordinator.enqueueOutbox(outbox); + + await expect(coordinator.receive({ ...inbox, content: 'api_key=secret-two' })).rejects.toThrow( + /idempotency conflict/, + ); + await expect( + coordinator.enqueueOutbox({ ...outbox, content: 'api_key=secret-two' }), + ).rejects.toThrow(/idempotency conflict/); + }, 30_000); + + it('rejects database inbox and outbox idempotency-key conflicts', async () => { + const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db)); + await coordinator.create(IDENTITY); + await coordinator.receive({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'inbox-conflict', + correlationId: 'correlation-inbox', + content: 'original inbox', + }); + await coordinator.enqueueOutbox({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'outbox-conflict', + correlationId: 'correlation-outbox', + channelId: 'cli', + kind: 'provider.send', + content: 'original outbox', + }); + + await expect( + coordinator.receive({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'inbox-conflict', + correlationId: 'forged-correlation', + content: 'original inbox', + }), + ).rejects.toThrow(/idempotency conflict/); + await expect( + coordinator.enqueueOutbox({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'outbox-conflict', + correlationId: 'correlation-outbox', + channelId: 'forged-channel', + kind: 'provider.send', + content: 'original outbox', + }), + ).rejects.toThrow(/idempotency conflict/); + }, 30_000); + + it('does not requeue a live outbox claim during a normal scoped dispatch', async () => { + const repository = new TessDurableSessionRepository(handle.db); + const coordinator = new DurableSessionCoordinator(repository); + const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) }; + const service = new TessDurableSessionService(repository, runtimeProviders as never); + const input = { + sessionId: IDENTITY.sessionId, + idempotencyKey: 'live-effect', + correlationId: 'correlation-live', + content: 'must not duplicate', + context: { + actorScope: { userId: IDENTITY.ownerId, tenantId: IDENTITY.tenantId }, + channelId: 'cli', + correlationId: 'correlation-live', + }, + }; + + await coordinator.create(IDENTITY); + await service.queueProviderSend(input); + expect(await repository.claimOutbox(IDENTITY.sessionId)).toMatchObject({ + status: 'processing', + }); + + await service.dispatchProviderOutbox(IDENTITY.sessionId, input); + await expect( + service.recoverProviderSession(IDENTITY.sessionId, { + ...input, + context: { + ...input.context, + actorScope: { userId: 'intruder', tenantId: 'tenant-pglite' }, + }, + }), + ).rejects.toThrow(/scope or correlation mismatch/); + + expect(runtimeProviders.sendMessage).not.toHaveBeenCalled(); + expect(await coordinator.snapshot(IDENTITY.sessionId)).toMatchObject({ + outbox: [{ idempotencyKey: 'live-effect', status: 'processing' }], + }); + }, 30_000); + + it('rejects an outbox correlation mismatch before claiming the pending effect', async () => { + const repository = new TessDurableSessionRepository(handle.db); + const coordinator = new DurableSessionCoordinator(repository); + const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) }; + const service = new TessDurableSessionService(repository, runtimeProviders as never); + const input = { + sessionId: IDENTITY.sessionId, + idempotencyKey: 'mismatch-effect', + correlationId: 'correlation-expected', + content: 'must remain pending', + context: { + actorScope: { userId: IDENTITY.ownerId, tenantId: IDENTITY.tenantId }, + channelId: 'cli', + correlationId: 'correlation-expected', + }, + }; + + await coordinator.create(IDENTITY); + await service.queueProviderSend(input); + await expect( + service.dispatchProviderOutbox(IDENTITY.sessionId, { + ...input, + correlationId: 'correlation-forged', + context: { ...input.context, correlationId: 'correlation-forged' }, + }), + ).rejects.toThrow(/scope or correlation mismatch/); + + expect(runtimeProviders.sendMessage).not.toHaveBeenCalled(); + expect(await coordinator.snapshot(IDENTITY.sessionId)).toMatchObject({ + outbox: [{ idempotencyKey: 'mismatch-effect', status: 'pending' }], + }); + }, 30_000); + + it('dispatches only the outbox record bound to the supplied correlation and channel', async () => { + const repository = new TessDurableSessionRepository(handle.db); + const coordinator = new DurableSessionCoordinator(repository); + const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) }; + const service = new TessDurableSessionService(repository, runtimeProviders as never); + const first = { + sessionId: IDENTITY.sessionId, + idempotencyKey: 'scoped-effect-one', + correlationId: 'correlation-one', + content: 'first result', + context: { + actorScope: { userId: IDENTITY.ownerId, tenantId: IDENTITY.tenantId }, + channelId: 'cli', + correlationId: 'correlation-one', + }, + }; + const second = { + ...first, + idempotencyKey: 'scoped-effect-two', + correlationId: 'correlation-two', + content: 'second result', + context: { ...first.context, correlationId: 'correlation-two' }, + }; + + await coordinator.create(IDENTITY); + await service.queueProviderSend(first); + await service.queueProviderSend(second); + await service.dispatchProviderOutbox(IDENTITY.sessionId, first); + + expect(runtimeProviders.sendMessage).toHaveBeenCalledTimes(1); + expect(runtimeProviders.sendMessage).toHaveBeenCalledWith( + IDENTITY.providerId, + IDENTITY.runtimeSessionId, + { content: 'first result', idempotencyKey: 'scoped-effect-one' }, + first.context, + ); + expect(await coordinator.snapshot(IDENTITY.sessionId)).toMatchObject({ + outbox: [ + { idempotencyKey: 'scoped-effect-one', status: 'delivered' }, + { idempotencyKey: 'scoped-effect-two', status: 'pending' }, + ], + }); + }, 30_000); +}); + +async function seedOwner(handle: DbHandle): Promise { + await handle.db.execute(sql` + INSERT INTO users (id, name, email, email_verified, created_at, updated_at) + VALUES ('tess-owner', 'Tess Owner', 'tess-owner@example.test', false, now(), now()) + `); +} diff --git a/apps/gateway/src/agent/tess-durable-session.repository.ts b/apps/gateway/src/agent/tess-durable-session.repository.ts new file mode 100644 index 0000000..52d853b --- /dev/null +++ b/apps/gateway/src/agent/tess-durable-session.repository.ts @@ -0,0 +1,530 @@ +import { createHash, createHmac } from 'node:crypto'; +import { Inject, Injectable } from '@nestjs/common'; +import { + and, + asc, + desc, + eq, + interactionCheckpoints, + interactionHandoffs, + interactionInbox, + interactionOutbox, + interactionSessions, + type Db, +} from '@mosaicstack/db'; +import { seal, unseal } from '@mosaicstack/auth'; +import { redactSensitiveContent } from '@mosaicstack/log'; +import type { + DurableCheckpoint, + DurableCheckpointInput, + DurableEnqueueResult, + DurableHandoff, + DurableHandoffInput, + DurableInboxEntry, + DurableInboxInput, + DurableInboxStatus, + DurableOutboxEntry, + DurableOutboxInput, + DurableOutboxStatus, + DurableSessionIdentity, + DurableSessionSnapshot, + DurableSessionStore, +} from '@mosaicstack/agent'; +import { DB } from '../database/database.module.js'; + +@Injectable() +export class TessDurableSessionRepository implements DurableSessionStore { + constructor(@Inject(DB) private readonly db: Db) {} + + async create(identity: DurableSessionIdentity): Promise { + await this.db + .insert(interactionSessions) + .values({ + id: identity.sessionId, + agentName: identity.agentName, + tenantId: identity.tenantId, + ownerId: identity.ownerId, + providerId: identity.providerId, + runtimeSessionId: identity.runtimeSessionId, + }) + .onConflictDoNothing(); + + const existing = await this.session(identity.sessionId); + if (!existing || !sameEnrollmentScope(existing, identity)) { + throw new Error(`Durable Tess session identity conflict: ${identity.sessionId}`); + } + // A recovered/re-enrolled runtime can receive a new provider session ID; + // the conversation handle and owner scope remain immutable. + if ( + existing.providerId !== identity.providerId || + existing.runtimeSessionId !== identity.runtimeSessionId + ) { + await this.db + .update(interactionSessions) + .set({ providerId: identity.providerId, runtimeSessionId: identity.runtimeSessionId }) + .where(eq(interactionSessions.id, identity.sessionId)); + } + } + + async snapshot(sessionId: string): Promise { + const identity = await this.session(sessionId); + if (!identity) return null; + + const [inbox, outbox, checkpoints, handoffs] = await Promise.all([ + this.db + .select() + .from(interactionInbox) + .where(eq(interactionInbox.sessionId, sessionId)) + .orderBy(asc(interactionInbox.createdAt)), + this.db + .select() + .from(interactionOutbox) + .where(eq(interactionOutbox.sessionId, sessionId)) + .orderBy(asc(interactionOutbox.createdAt)), + this.db + .select() + .from(interactionCheckpoints) + .where(eq(interactionCheckpoints.sessionId, sessionId)) + .orderBy( + desc(interactionCheckpoints.compactionEpoch), + desc(interactionCheckpoints.createdAt), + ) + .limit(1), + this.db + .select() + .from(interactionHandoffs) + .where(eq(interactionHandoffs.sessionId, sessionId)) + .orderBy(asc(interactionHandoffs.createdAt)), + ]); + + const checkpoint = checkpoints[0]; + return { + identity, + inbox: inbox.map(toInbox), + outbox: outbox.map(toOutbox), + ...(checkpoint ? { checkpoint: toCheckpoint(checkpoint) } : {}), + handoffs: handoffs.map(toHandoff), + }; + } + + async enqueueInbox(input: DurableInboxInput): Promise> { + const digest = contentDigest(input.content); + const record: DurableInboxInput = { + ...input, + content: redactSensitiveContent(input.content).content, + }; + const inserted = await this.db + .insert(interactionInbox) + .values({ + ...record, + content: seal(record.content), + contentDigest: digest, + status: 'pending', + }) + .onConflictDoNothing() + .returning({ status: interactionInbox.status }); + if (inserted[0]) return { accepted: true, status: inserted[0].status }; + + const existing = await this.db + .select() + .from(interactionInbox) + .where( + and( + eq(interactionInbox.sessionId, input.sessionId), + eq(interactionInbox.idempotencyKey, input.idempotencyKey), + ), + ) + .limit(1); + if (!existing[0]) throw new Error(`Durable Tess inbox enqueue failed: ${input.idempotencyKey}`); + const entry = toInbox(existing[0]); + if ( + !sameInbox(entry, record) || + !matchesContentDigest(existing[0].contentDigest, input.content) + ) { + throw new Error(`Durable Tess inbox idempotency conflict: ${input.idempotencyKey}`); + } + return { accepted: false, status: entry.status }; + } + + async claimInbox(sessionId: string): Promise { + for (let attempt = 0; attempt < 3; attempt += 1) { + const candidate = await this.db + .select() + .from(interactionInbox) + .where( + and(eq(interactionInbox.sessionId, sessionId), eq(interactionInbox.status, 'pending')), + ) + .orderBy(asc(interactionInbox.createdAt)) + .limit(1); + const entry = candidate[0]; + if (!entry) return null; + const claimed = await this.db + .update(interactionInbox) + .set({ status: 'processing', updatedAt: new Date() }) + .where(and(eq(interactionInbox.id, entry.id), eq(interactionInbox.status, 'pending'))) + .returning(); + if (claimed[0]) return toInbox(claimed[0]); + } + return null; + } + + async completeInbox(sessionId: string, idempotencyKey: string): Promise { + await this.db + .update(interactionInbox) + .set({ status: 'processed', updatedAt: new Date() }) + .where( + and( + eq(interactionInbox.sessionId, sessionId), + eq(interactionInbox.idempotencyKey, idempotencyKey), + eq(interactionInbox.status, 'processing'), + ), + ); + } + + async releaseInbox(sessionId: string, idempotencyKey: string): Promise { + await this.db + .update(interactionInbox) + .set({ status: 'pending', updatedAt: new Date() }) + .where( + and( + eq(interactionInbox.sessionId, sessionId), + eq(interactionInbox.idempotencyKey, idempotencyKey), + eq(interactionInbox.status, 'processing'), + ), + ); + } + + async enqueueOutbox( + input: DurableOutboxInput, + ): Promise> { + const digest = contentDigest(input.content); + const record: DurableOutboxInput = { + ...input, + content: redactSensitiveContent(input.content).content, + }; + const inserted = await this.db + .insert(interactionOutbox) + .values({ + ...record, + content: seal(record.content), + contentDigest: digest, + status: 'pending', + }) + .onConflictDoNothing() + .returning({ status: interactionOutbox.status }); + if (inserted[0]) return { accepted: true, status: inserted[0].status }; + + const existing = await this.db + .select() + .from(interactionOutbox) + .where( + and( + eq(interactionOutbox.sessionId, input.sessionId), + eq(interactionOutbox.idempotencyKey, input.idempotencyKey), + ), + ) + .limit(1); + if (!existing[0]) + throw new Error(`Durable Tess outbox enqueue failed: ${input.idempotencyKey}`); + const entry = toOutbox(existing[0]); + if ( + !sameOutbox(entry, record) || + !matchesContentDigest(existing[0].contentDigest, input.content) + ) { + throw new Error(`Durable Tess outbox idempotency conflict: ${input.idempotencyKey}`); + } + return { accepted: false, status: entry.status }; + } + + async claimOutbox(sessionId: string): Promise { + for (let attempt = 0; attempt < 3; attempt += 1) { + const candidate = await this.db + .select() + .from(interactionOutbox) + .where( + and(eq(interactionOutbox.sessionId, sessionId), eq(interactionOutbox.status, 'pending')), + ) + .orderBy(asc(interactionOutbox.createdAt)) + .limit(1); + const entry = candidate[0]; + if (!entry) return null; + const claimed = await this.db + .update(interactionOutbox) + .set({ status: 'processing', updatedAt: new Date() }) + .where(and(eq(interactionOutbox.id, entry.id), eq(interactionOutbox.status, 'pending'))) + .returning(); + if (claimed[0]) return toOutbox(claimed[0]); + } + return null; + } + + async claimOutboxByKey( + sessionId: string, + idempotencyKey: string, + ): Promise { + const claimed = await this.db + .update(interactionOutbox) + .set({ status: 'processing', updatedAt: new Date() }) + .where( + and( + eq(interactionOutbox.sessionId, sessionId), + eq(interactionOutbox.idempotencyKey, idempotencyKey), + eq(interactionOutbox.status, 'pending'), + ), + ) + .returning(); + return claimed[0] ? toOutbox(claimed[0]) : null; + } + + async completeOutbox(sessionId: string, idempotencyKey: string): Promise { + await this.db + .update(interactionOutbox) + .set({ status: 'delivered', updatedAt: new Date() }) + .where( + and( + eq(interactionOutbox.sessionId, sessionId), + eq(interactionOutbox.idempotencyKey, idempotencyKey), + eq(interactionOutbox.status, 'processing'), + ), + ); + } + + async releaseOutbox(sessionId: string, idempotencyKey: string): Promise { + await this.db + .update(interactionOutbox) + .set({ status: 'pending', updatedAt: new Date() }) + .where( + and( + eq(interactionOutbox.sessionId, sessionId), + eq(interactionOutbox.idempotencyKey, idempotencyKey), + eq(interactionOutbox.status, 'processing'), + ), + ); + } + + async checkpoint(input: DurableCheckpointInput): Promise { + // Compute identity before redaction. The persisted digest is keyed so a database + // reader cannot use it as an offline oracle for sensitive cursor/summary values. + const digest = contentDigest(JSON.stringify([input.cursor, input.summary])); + const checkpoint: DurableCheckpointInput = { + ...input, + cursor: redactSensitiveContent(input.cursor).content, + summary: redactSensitiveContent(input.summary).content, + }; + const inserted = await this.db + .insert(interactionCheckpoints) + .values({ + ...checkpoint, + contentDigest: digest, + cursor: seal(checkpoint.cursor), + summary: seal(checkpoint.summary), + }) + .onConflictDoNothing() + .returning({ checkpointId: interactionCheckpoints.checkpointId }); + if (inserted[0]) return; + + const existing = await this.db + .select() + .from(interactionCheckpoints) + .where( + and( + eq(interactionCheckpoints.sessionId, input.sessionId), + eq(interactionCheckpoints.checkpointId, input.checkpointId), + ), + ) + .limit(1); + if ( + !existing[0] || + !sameCheckpoint(toCheckpoint(existing[0]), checkpoint) || + !matchesCheckpointDigest(existing[0].contentDigest, digest) + ) { + throw new Error(`Durable Tess checkpoint identity conflict: ${input.checkpointId}`); + } + } + + async findCheckpoint(sessionId: string, checkpointId: string): Promise { + const checkpoints = await this.db + .select() + .from(interactionCheckpoints) + .where( + and( + eq(interactionCheckpoints.sessionId, sessionId), + eq(interactionCheckpoints.checkpointId, checkpointId), + ), + ) + .limit(1); + const checkpoint = checkpoints[0]; + return checkpoint ? toCheckpoint(checkpoint) : null; + } + + async handoff(input: DurableHandoffInput): Promise { + const checkpoint = await this.findCheckpoint(input.sessionId, input.checkpointId); + if (!checkpoint) { + throw new Error(`Durable Tess handoff checkpoint is unavailable: ${input.checkpointId}`); + } + const inserted = await this.db + .insert(interactionHandoffs) + .values({ ...input }) + .onConflictDoNothing() + .returning({ handoffId: interactionHandoffs.handoffId }); + if (inserted[0]) return; + + const existing = await this.findHandoff(input.handoffId); + if (!existing || !sameHandoff(existing, input)) { + throw new Error(`Durable Tess handoff identity conflict: ${input.handoffId}`); + } + } + + async findHandoff(handoffId: string): Promise { + const handoffs = await this.db + .select() + .from(interactionHandoffs) + .where(eq(interactionHandoffs.handoffId, handoffId)) + .limit(1); + const handoff = handoffs[0]; + return handoff ? toHandoff(handoff) : null; + } + + async requeueInFlight(sessionId: string): Promise { + // Inbox handlers are process-local work. A provider outbox claim may have + // reached an external target before a crash, so it is deliberately not + // replayed by generic recovery. + await this.db + .update(interactionInbox) + .set({ status: 'pending', updatedAt: new Date() }) + .where( + and(eq(interactionInbox.sessionId, sessionId), eq(interactionInbox.status, 'processing')), + ); + } + + private async session(sessionId: string): Promise { + const sessions = await this.db + .select() + .from(interactionSessions) + .where(eq(interactionSessions.id, sessionId)) + .limit(1); + const session = sessions[0]; + return session + ? { + agentName: session.agentName, + sessionId: session.id, + tenantId: session.tenantId, + ownerId: session.ownerId, + providerId: session.providerId, + runtimeSessionId: session.runtimeSessionId, + } + : null; + } +} + +function contentDigest(content: string): string { + const secret = process.env['BETTER_AUTH_SECRET']; + if (!secret) { + throw new Error('BETTER_AUTH_SECRET is required for durable idempotency digests'); + } + return `hmac:v1:${createHmac('sha256', secret).update(content).digest('hex')}`; +} + +function matchesContentDigest(stored: string, content: string): boolean { + return ( + stored === contentDigest(content) || + stored === createHash('sha256').update(content).digest('hex') + ); +} + +function matchesCheckpointDigest(stored: string, digest: string): boolean { + // Legacy rows predate any pre-redaction identity and cannot safely prove equality. + // Reject rather than let redaction collapse distinct sensitive checkpoint payloads. + return stored === digest; +} + +function sameEnrollmentScope(left: DurableSessionIdentity, right: DurableSessionIdentity): boolean { + return ( + left.agentName === right.agentName && + left.sessionId === right.sessionId && + left.tenantId === right.tenantId && + left.ownerId === right.ownerId + ); +} + +function sameInbox(left: DurableInboxEntry, right: DurableInboxInput): boolean { + return ( + left.sessionId === right.sessionId && + left.idempotencyKey === right.idempotencyKey && + left.correlationId === right.correlationId && + left.content === right.content + ); +} + +function sameOutbox(left: DurableOutboxEntry, right: DurableOutboxInput): boolean { + return ( + left.sessionId === right.sessionId && + left.idempotencyKey === right.idempotencyKey && + left.correlationId === right.correlationId && + left.channelId === right.channelId && + left.kind === right.kind && + left.content === right.content + ); +} + +function sameCheckpoint(left: DurableCheckpoint, right: DurableCheckpointInput): boolean { + return ( + left.sessionId === right.sessionId && + left.checkpointId === right.checkpointId && + left.compactionEpoch === right.compactionEpoch + ); +} + +function sameHandoff(left: DurableHandoff, right: DurableHandoffInput): boolean { + return ( + left.sessionId === right.sessionId && + left.handoffId === right.handoffId && + left.destination === right.destination && + left.correlationId === right.correlationId && + left.checkpointId === right.checkpointId && + left.status === right.status + ); +} + +function toInbox(row: typeof interactionInbox.$inferSelect): DurableInboxEntry { + return { + sessionId: row.sessionId, + idempotencyKey: row.idempotencyKey, + correlationId: row.correlationId, + content: unseal(row.content), + status: row.status, + }; +} + +function toOutbox(row: typeof interactionOutbox.$inferSelect): DurableOutboxEntry { + return { + sessionId: row.sessionId, + idempotencyKey: row.idempotencyKey, + correlationId: row.correlationId, + channelId: row.channelId, + kind: row.kind, + content: unseal(row.content), + status: row.status, + }; +} + +function toCheckpoint(row: typeof interactionCheckpoints.$inferSelect): DurableCheckpoint { + return { + sessionId: row.sessionId, + checkpointId: row.checkpointId, + cursor: unseal(row.cursor), + summary: unseal(row.summary), + compactionEpoch: row.compactionEpoch, + }; +} + +function toHandoff(row: typeof interactionHandoffs.$inferSelect): DurableHandoff { + return { + sessionId: row.sessionId, + handoffId: row.handoffId, + destination: row.destination, + correlationId: row.correlationId, + checkpointId: row.checkpointId, + status: row.status, + }; +} diff --git a/apps/gateway/src/agent/tess-durable-session.service.ts b/apps/gateway/src/agent/tess-durable-session.service.ts new file mode 100644 index 0000000..340694f --- /dev/null +++ b/apps/gateway/src/agent/tess-durable-session.service.ts @@ -0,0 +1,123 @@ +import { ForbiddenException, Inject, Injectable } from '@nestjs/common'; +import { DurableSessionCoordinator, type DurableSessionIdentity } from '@mosaicstack/agent'; +import type { TessProviderOutboxDto } from './tess-durable-session.dto.js'; +import { TessDurableSessionRepository } from './tess-durable-session.repository.js'; +import { + RuntimeProviderService, + type RuntimeProviderRequestContext, +} from './runtime-provider-registry.service.js'; + +/** + * Scoped gateway boundary for the canonical Tess state machine. It deliberately + * uses composition: raw state methods cannot be injected into channel, CLI, or + * MCP adapters without a server-derived actor/tenant/correlation context. + */ +@Injectable() +export class TessDurableSessionService { + private readonly coordinator: DurableSessionCoordinator; + + constructor( + @Inject(TessDurableSessionRepository) repository: TessDurableSessionRepository, + @Inject(RuntimeProviderService) private readonly runtimeProviders: RuntimeProviderService, + ) { + this.coordinator = new DurableSessionCoordinator(repository); + } + + /** Enroll a verified runtime session under the stable cross-surface conversation handle. */ + async enroll( + identity: DurableSessionIdentity, + context: RuntimeProviderRequestContext, + ): Promise { + if ( + identity.ownerId !== context.actorScope.userId || + identity.tenantId !== context.actorScope.tenantId + ) { + throw new ForbiddenException('Durable Tess enrollment scope mismatch'); + } + await this.coordinator.create(identity); + } + + async queueProviderSend(input: TessProviderOutboxDto): Promise { + const snapshot = await this.coordinator.snapshot(input.sessionId); + this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input); + await this.coordinator.enqueueOutbox({ + sessionId: input.sessionId, + idempotencyKey: input.idempotencyKey, + correlationId: input.correlationId, + channelId: input.context.channelId, + kind: 'provider.send', + content: input.content, + }); + } + + async dispatchProviderOutbox(sessionId: string, input: TessProviderOutboxDto): Promise { + if (sessionId !== input.sessionId) { + throw new ForbiddenException('Durable Tess outbox session mismatch'); + } + const snapshot = await this.coordinator.snapshot(sessionId); + this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input); + const pendingEntry = snapshot.outbox.find( + (entry): boolean => entry.idempotencyKey === input.idempotencyKey, + ); + if (!pendingEntry) return; + // Validate immutable routing before claiming. A caller with a mismatched + // correlation/channel must not strand a pending external side effect. + this.assertOutboxScope(pendingEntry, input); + await this.coordinator.dispatchOutboxEntry( + sessionId, + input.idempotencyKey, + async (entry): Promise => { + this.assertOutboxScope(entry, input); + await this.runtimeProviders.sendMessage( + snapshot.identity.providerId, + snapshot.identity.runtimeSessionId, + { content: entry.content, idempotencyKey: entry.idempotencyKey }, + input.context, + ); + }, + ); + } + + /** Read durable identity/state only after deriving and checking the server-side actor scope. */ + async getSnapshot(sessionId: string, context: RuntimeProviderRequestContext) { + const snapshot = await this.coordinator.snapshot(sessionId); + this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, { + sessionId, + content: '', + idempotencyKey: 'read-only', + correlationId: context.correlationId, + context, + }); + return snapshot; + } + + /** Startup/recovery-only path; normal queue/dispatch methods never requeue live work. */ + async recoverProviderSession(sessionId: string, input: TessProviderOutboxDto): Promise { + const snapshot = await this.coordinator.snapshot(sessionId); + this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input); + await this.coordinator.recover(sessionId); + } + + private assertOutboxScope( + entry: { kind: string; correlationId: string; channelId: string }, + input: TessProviderOutboxDto, + ): void { + if ( + entry.kind !== 'provider.send' || + entry.correlationId !== input.correlationId || + entry.channelId !== input.context.channelId + ) { + throw new ForbiddenException('Durable Tess outbox scope or correlation mismatch'); + } + } + + private assertScope(ownerId: string, tenantId: string, input: TessProviderOutboxDto): void { + if ( + input.context.actorScope.userId !== ownerId || + input.context.actorScope.tenantId !== tenantId || + input.context.correlationId !== input.correlationId + ) { + throw new ForbiddenException('Durable Tess session scope or correlation mismatch'); + } + } +} diff --git a/apps/gateway/src/agent/tools/memory-tools.test.ts b/apps/gateway/src/agent/tools/memory-tools.test.ts new file mode 100644 index 0000000..d90e7bf --- /dev/null +++ b/apps/gateway/src/agent/tools/memory-tools.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createMemoryTools } from './memory-tools.js'; + +describe('createMemoryTools operator retrieval binding', () => { + const memory = { + insights: { searchByEmbedding: vi.fn(), create: vi.fn() }, + preferences: { findByUserAndCategory: vi.fn(), findByUser: vi.fn(), upsert: vi.fn() }, + }; + const scope = { tenantId: 'tenant-a', ownerId: 'owner-a', sessionId: 'session-a' }; + + it('uses the configured plugin with the server-derived scope for retrieval and capture', async () => { + const plugin = { + search: vi.fn(async () => []), + capture: vi.fn(async () => ({ id: 'insight-1' })), + }; + const tools = createMemoryTools(memory as never, null, 'owner-a', { + plugin: plugin as never, + scope, + }); + + await tools + .find((tool) => tool.name === 'memory_search')! + .execute('call-1', { query: 'plans' }, undefined, undefined, {} as never); + await tools + .find((tool) => tool.name === 'memory_save_insight')! + .execute( + 'call-2', + { content: 'secret', category: 'decision' }, + undefined, + undefined, + {} as never, + ); + + expect(plugin.search).toHaveBeenCalledWith(scope, 'plans', 5); + expect(plugin.capture).toHaveBeenCalledWith(scope, { + content: 'secret', + source: 'agent', + category: 'decision', + }); + }); +}); diff --git a/apps/gateway/src/agent/tools/memory-tools.ts b/apps/gateway/src/agent/tools/memory-tools.ts index ab1809a..ec9b744 100644 --- a/apps/gateway/src/agent/tools/memory-tools.ts +++ b/apps/gateway/src/agent/tools/memory-tools.ts @@ -1,7 +1,11 @@ import { Type } from '@sinclair/typebox'; import type { ToolDefinition } from '@mariozechner/pi-coding-agent'; -import type { Memory } from '@mosaicstack/memory'; -import type { EmbeddingProvider } from '@mosaicstack/memory'; +import type { + EmbeddingProvider, + Memory, + OperatorMemoryPlugin, + OperatorMemoryScope, +} from '@mosaicstack/memory'; /** * Create memory tools bound to the session's authenticated userId. @@ -13,8 +17,10 @@ import type { EmbeddingProvider } from '@mosaicstack/memory'; export function createMemoryTools( memory: Memory, embeddingProvider: EmbeddingProvider | null, - /** Authenticated user ID from the session. All memory operations are scoped to this user. */ + /** Authenticated user ID from the session. All preference operations are scoped to this user. */ sessionUserId: string | undefined, + /** Optional configured retrieval plugin, bound to a server-derived session scope. */ + operatorMemory?: { plugin: OperatorMemoryPlugin; scope: OperatorMemoryScope }, ): ToolDefinition[] { /** Return an error result when no session user is bound. */ function noUserError() { @@ -46,6 +52,14 @@ export function createMemoryTools( limit?: number; }; + if (operatorMemory) { + const results = await operatorMemory.plugin.search(operatorMemory.scope, query, limit ?? 5); + return { + content: [{ type: 'text' as const, text: JSON.stringify(results, null, 2) }], + details: undefined, + }; + } + if (!embeddingProvider) { return { content: [ @@ -158,6 +172,18 @@ export function createMemoryTools( }; type Cat = 'decision' | 'learning' | 'preference' | 'fact' | 'pattern' | 'general'; + if (operatorMemory) { + const insight = await operatorMemory.plugin.capture(operatorMemory.scope, { + content, + source: 'agent', + category: category ?? 'learning', + }); + return { + content: [{ type: 'text' as const, text: JSON.stringify(insight, null, 2) }], + details: undefined, + }; + } + let embedding: number[] | null = null; if (embeddingProvider) { embedding = await embeddingProvider.embed(content); diff --git a/apps/gateway/src/chat/chat.gateway.ts b/apps/gateway/src/chat/chat.gateway.ts index 7b4f0b3..a7549e4 100644 --- a/apps/gateway/src/chat/chat.gateway.ts +++ b/apps/gateway/src/chat/chat.gateway.ts @@ -1,4 +1,5 @@ -import { Inject, Logger } from '@nestjs/common'; +import { createHash } from 'node:crypto'; +import { Inject, Logger, Optional } from '@nestjs/common'; import { WebSocketGateway, WebSocketServer, @@ -13,6 +14,9 @@ import { Server, Socket } from 'socket.io'; import type { AgentSessionEvent } from '@mariozechner/pi-coding-agent'; import { verifyDiscordIngressEnvelope, + parseDiscordInteractionBindings, + resolveDiscordInteractionActorId, + resolveDiscordInteractionBinding, type DiscordIngressEnvelope, type DiscordIngressPayload, } from '@mosaicstack/discord-plugin'; @@ -28,6 +32,12 @@ import type { AbortPayload, } from '@mosaicstack/types'; import { AgentService, type ConversationHistoryMessage } from '../agent/agent.service.js'; +import { + RUNTIME_PROVIDER_AUDIT_SINK, + RuntimeProviderService, + type RuntimeAuditSink, +} from '../agent/runtime-provider-registry.service.js'; +import { TessDurableSessionService } from '../agent/tess-durable-session.service.js'; import { AUTH } from '../auth/auth.tokens.js'; import { scopeFromUser, @@ -37,6 +47,7 @@ import { import { BRAIN } from '../brain/brain.tokens.js'; import { CommandRegistryService } from '../commands/command-registry.service.js'; import { CommandExecutorService } from '../commands/command-executor.service.js'; +import { CommandAuthorizationService } from '../commands/command-authorization.service.js'; import { RoutingEngineService } from '../agent/routing/routing-engine.service.js'; import { v4 as uuid } from 'uuid'; import { ChatSocketMessageDto } from './chat.dto.js'; @@ -122,6 +133,18 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa @Inject(CommandRegistryService) private readonly commandRegistry: CommandRegistryService, @Inject(CommandExecutorService) private readonly commandExecutor: CommandExecutorService, @Inject(RoutingEngineService) private readonly routingEngine: RoutingEngineService, + @Optional() + @Inject(CommandAuthorizationService) + private readonly commandAuthorization: CommandAuthorizationService | null = null, + @Optional() + @Inject(RuntimeProviderService) + private readonly runtimeRegistry: RuntimeProviderService | null = null, + @Optional() + @Inject(TessDurableSessionService) + private readonly durableSessions: TessDurableSessionService | null = null, + @Optional() + @Inject(RUNTIME_PROVIDER_AUDIT_SINK) + private readonly runtimeAudit: RuntimeAuditSink | null = null, ) {} afterInit(): void { @@ -637,9 +660,185 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa * Creates it if absent — safe to call concurrently since a duplicate insert * would fail on the PK constraint and be caught here. */ + @SubscribeMessage('discord:approve') + async handleDiscordApproval( + @ConnectedSocket() client: Socket, + @MessageBody() envelope: DiscordIngressEnvelope, + ): Promise { + if (!client.data.discordService) return; + const ingress = this.resolveDiscordIngress(client, envelope, 'approve'); + const isApprovalCommand = /^\/approve\s*$/i.test(ingress?.content ?? ''); + const tenantId = process.env['DISCORD_SERVICE_TENANT_ID']?.trim(); + if ( + !ingress || + !isApprovalCommand || + !tenantId || + !this.commandAuthorization || + !this.durableSessions + ) + return; + const binding = resolveDiscordInteractionBinding( + parseDiscordInteractionBindings(process.env['DISCORD_INTERACTION_BINDINGS']), + ingress.guildId, + ingress.channelId, + ingress.userId, + 'approve', + ); + const actorId = binding && resolveDiscordInteractionActorId(binding, ingress.userId); + const agentName = process.env['MOSAIC_AGENT_NAME']?.trim(); + if (!actorId || !agentName || binding.instanceId !== agentName) { + this.logger.warn( + `Rejected Discord approval without a matching runtime agent from ${client.id}`, + ); + client.emit('discord:approval', { + correlationId: ingress.correlationId, + success: false, + approvalId: undefined, + expiresAt: undefined, + }); + return; + } + let snapshot; + try { + snapshot = await this.durableSessions.getSnapshot(ingress.conversationId, { + actorScope: { userId: actorId, tenantId }, + channelId: ingress.channelId, + correlationId: ingress.correlationId, + }); + } catch { + client.emit('discord:approval', { + correlationId: ingress.correlationId, + success: false, + approvalId: undefined, + expiresAt: undefined, + }); + return; + } + if (snapshot.identity.agentName !== agentName) { + client.emit('discord:approval', { + correlationId: ingress.correlationId, + success: false, + approvalId: undefined, + expiresAt: undefined, + }); + return; + } + const approval = await this.commandAuthorization.createRuntimeTerminationApproval({ + providerId: snapshot.identity.providerId, + sessionId: snapshot.identity.runtimeSessionId, + actorId, + tenantId, + channelId: ingress.channelId, + correlationId: this.discordRuntimeActionCorrelation( + binding.instanceId, + ingress, + snapshot.identity.providerId, + snapshot.identity.runtimeSessionId, + ), + agentName, + }); + if (!approval) { + await this.runtimeAudit?.record({ + providerId: snapshot.identity.providerId, + operation: 'session.terminate', + outcome: 'denied', + actorId, + tenantId, + channelId: ingress.channelId, + correlationId: this.discordRuntimeActionCorrelation( + binding.instanceId, + ingress, + snapshot.identity.providerId, + snapshot.identity.runtimeSessionId, + ), + resourceId: snapshot.identity.runtimeSessionId, + errorCode: 'policy_denied', + }); + } + client.emit('discord:approval', { + correlationId: ingress.correlationId, + success: approval !== null, + approvalId: approval?.approvalId, + expiresAt: approval?.expiresAt, + }); + } + + @SubscribeMessage('discord:stop') + async handleDiscordStop( + @ConnectedSocket() client: Socket, + @MessageBody() envelope: DiscordIngressEnvelope, + ): Promise { + if (!client.data.discordService) return; + const ingress = this.resolveDiscordIngress(client, envelope, 'stop'); + const approvalRef = /^\/stop\s+([^\s]+)$/i.exec(ingress?.content ?? '')?.[1]; + const tenantId = process.env['DISCORD_SERVICE_TENANT_ID']?.trim(); + if (!ingress || !approvalRef || !tenantId || !this.runtimeRegistry || !this.durableSessions) + return; + const binding = resolveDiscordInteractionBinding( + parseDiscordInteractionBindings(process.env['DISCORD_INTERACTION_BINDINGS']), + ingress.guildId, + ingress.channelId, + ingress.userId, + 'stop', + ); + const actorId = binding && resolveDiscordInteractionActorId(binding, ingress.userId); + if (!actorId) return; + + try { + const context = { + actorScope: { userId: actorId, tenantId }, + channelId: ingress.channelId, + correlationId: ingress.correlationId, + }; + const snapshot = await this.durableSessions.getSnapshot(ingress.conversationId, context); + if (snapshot.identity.agentName !== binding.instanceId) throw new Error('agent mismatch'); + // RuntimeProviderService consumes the durable approval exactly once using the + // provisioned approving-admin identity, never the Discord service account. + await this.runtimeRegistry.terminate( + snapshot.identity.providerId, + snapshot.identity.runtimeSessionId, + approvalRef, + { + ...context, + correlationId: this.discordRuntimeActionCorrelation( + binding.instanceId, + ingress, + snapshot.identity.providerId, + snapshot.identity.runtimeSessionId, + ), + }, + ); + client.emit('discord:stop', { correlationId: ingress.correlationId, success: true }); + } catch { + client.emit('discord:stop', { correlationId: ingress.correlationId, success: false }); + } + } + + /** + * Correlates the immutable termination target rather than either Discord message. + * Approval and stop are distinct ingress events, but must consume the same seven-field action. + */ + private discordRuntimeActionCorrelation( + instanceId: string, + ingress: DiscordIngressPayload, + providerId: string, + sessionId: string, + ): string { + const target = [ + instanceId, + ingress.guildId, + ingress.channelId, + ingress.conversationId, + providerId, + sessionId, + ]; + return `discord-action:v1:${createHash('sha256').update(JSON.stringify(target)).digest('hex')}`; + } + private resolveDiscordIngress( client: Socket, envelope: DiscordIngressEnvelope, + operation: 'send' | 'approve' | 'stop' = 'send', ): DiscordIngressPayload | null { const payload = verifyDiscordIngressEnvelope( envelope, @@ -654,6 +853,24 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa this.logger.warn(`Rejected invalid Discord ingress envelope from ${client.id}`); return null; } + try { + const binding = resolveDiscordInteractionBinding( + parseDiscordInteractionBindings(process.env['DISCORD_INTERACTION_BINDINGS']), + payload.guildId, + payload.channelId, + payload.userId, + operation, + ); + if (!binding) { + this.logger.warn(`Rejected unpaired Discord ingress from ${client.id}`); + return null; + } + } catch { + this.logger.warn( + `Rejected Discord ingress without valid binding configuration from ${client.id}`, + ); + return null; + } if (!this.discordReplayProtector.claim(payload.messageId)) { this.logger.warn( `Rejected replayed Discord message=${payload.messageId} correlation=${payload.correlationId}`, diff --git a/apps/gateway/src/commands/command-authorization.service.spec.ts b/apps/gateway/src/commands/command-authorization.service.spec.ts index 7eb7034..315d902 100644 --- a/apps/gateway/src/commands/command-authorization.service.spec.ts +++ b/apps/gateway/src/commands/command-authorization.service.spec.ts @@ -12,8 +12,10 @@ const adminCommand: CommandDef = { }; const payload: SlashCommandPayload = { command: 'gc', conversationId: 'conversation-1' }; -function createService(role: string): CommandAuthorizationService { - const entries = new Map(); +function createService( + role: string, + entries: Map = new Map(), +): CommandAuthorizationService { const db = { select: () => ({ from: () => ({ where: () => ({ limit: async () => [{ role }] }) }) }), }; @@ -58,4 +60,57 @@ describe('CommandAuthorizationService', () => { (await service.authorize(adminCommand, payload, 'member-1', 'forged-approval-id')).allowed, ).toBe(false); }); + + it('denies a malformed durable approval expiry instead of treating it as unexpired', async (): Promise => { + const entries = new Map(); + const action = { + providerId: 'fleet', + sessionId: 'nova', + actorId: 'admin-1', + tenantId: 'tenant-1', + channelId: 'discord:operator', + correlationId: 'correlation-malformed-expiry', + agentName: 'Nova', + }; + const service = createService('admin', entries); + const approval = await service.createRuntimeTerminationApproval(action); + expect(approval).not.toBeNull(); + const key = `agent:Nova:command-approval:${approval!.approvalId}`; + const stored = entries.get(key); + expect(stored).toBeDefined(); + entries.set(key, JSON.stringify({ ...JSON.parse(stored!), expiresAt: 'not-a-date' })); + + expect(await service.consumeRuntimeTerminationApproval(approval!.approvalId, action)).toBe( + false, + ); + }); + + it('persists and consumes one exact runtime termination approval across a service restart', async (): Promise => { + const entries = new Map(); + const action = { + providerId: 'fleet', + sessionId: 'nova', + actorId: 'admin-1', + tenantId: 'tenant-1', + channelId: 'discord:operator', + correlationId: 'correlation-1', + agentName: 'Nova', + }; + const beforeRestart = createService('admin', entries); + const approval = await beforeRestart.createRuntimeTerminationApproval(action); + + const afterRestart = createService('admin', entries); + expect( + await afterRestart.consumeRuntimeTerminationApproval(approval!.approvalId, { + ...action, + sessionId: 'forged-session', + }), + ).toBe(false); + expect(await afterRestart.consumeRuntimeTerminationApproval(approval!.approvalId, action)).toBe( + true, + ); + expect(await afterRestart.consumeRuntimeTerminationApproval(approval!.approvalId, action)).toBe( + false, + ); + }); }); diff --git a/apps/gateway/src/commands/command-authorization.service.ts b/apps/gateway/src/commands/command-authorization.service.ts index ce7f499..a9f829e 100644 --- a/apps/gateway/src/commands/command-authorization.service.ts +++ b/apps/gateway/src/commands/command-authorization.service.ts @@ -15,6 +15,24 @@ export interface CommandApproval { expiresAt: string; } +/** Exact immutable binding for a privileged runtime termination. */ +export interface RuntimeTerminationApprovalAction { + providerId: string; + sessionId: string; + actorId: string; + tenantId: string; + channelId: string; + correlationId: string; + /** Provisioned roster identity; isolates approvals between interaction agents. */ + agentName: string; +} + +export interface RuntimeTerminationApproval extends RuntimeTerminationApprovalAction { + approvalId: string; + actionDigest: string; + expiresAt: string; +} + export interface CommandAuthorizationResult { allowed: boolean; reason?: string; @@ -75,6 +93,57 @@ export class CommandAuthorizationService { return approval; } + /** + * Uses the same `interaction:command-approval:*` store and one-time deletion rule as + * command approvals. This deliberately avoids a parallel approval database. + */ + async createRuntimeTerminationApproval( + action: RuntimeTerminationApprovalAction, + ): Promise { + if (!this.hasRuntimeTerminationAction(action)) return null; + const role = await this.resolveRole(action.actorId); + if (role !== 'admin') return null; + + const approval: RuntimeTerminationApproval = { + approvalId: randomUUID(), + actionDigest: this.runtimeActionDigest(action), + ...action, + expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(), + }; + await this.redis.set( + this.runtimeKey(action.agentName, approval.approvalId), + JSON.stringify(approval), + 'EX', + '300', + ); + return approval; + } + + async consumeRuntimeTerminationApproval( + approvalId: string, + action: RuntimeTerminationApprovalAction, + ): Promise { + const encoded = await this.redis.get(this.runtimeKey(action.agentName, approvalId)); + if (!encoded) return false; + let approval: unknown; + try { + approval = JSON.parse(encoded); + } catch { + return false; + } + if ( + !this.isRuntimeTerminationApproval(approval) || + approval.actionDigest !== this.runtimeActionDigest(action) || + approval.actorId !== action.actorId || + approval.tenantId !== action.tenantId || + !this.isUnexpired(approval.expiresAt) + ) { + return false; + } + if ((await this.resolveRole(approval.actorId)) !== 'admin') return false; + return (await this.redis.del(this.runtimeKey(action.agentName, approvalId))) === 1; + } + private async resolveRole(actorId: string): Promise { const [user] = await this.db .select({ role: usersTable.role }) @@ -98,12 +167,17 @@ export class CommandAuthorizationService { const key = this.key(approvalId); const encoded = await this.redis.get(key); if (!encoded) return false; - const parsed: unknown = JSON.parse(encoded); + let parsed: unknown; + try { + parsed = JSON.parse(encoded); + } catch { + return false; + } if ( - !this.isApproval(parsed) || + !this.isCommandApproval(parsed) || parsed.actorId !== actorId || parsed.actionDigest !== actionDigest || - Date.parse(parsed.expiresAt) <= Date.now() + !this.isUnexpired(parsed.expiresAt) ) return false; return (await this.redis.del(key)) === 1; @@ -121,18 +195,74 @@ export class CommandAuthorizationService { .digest('hex'); } - private isApproval(value: unknown): value is CommandApproval { + private hasRuntimeTerminationAction(action: RuntimeTerminationApprovalAction): boolean { + return [ + action.providerId, + action.sessionId, + action.actorId, + action.tenantId, + action.channelId, + action.correlationId, + action.agentName, + ].every((value: string): boolean => value.trim().length > 0); + } + + private runtimeActionDigest(action: RuntimeTerminationApprovalAction): string { + return createHash('sha256') + .update( + JSON.stringify({ + providerId: action.providerId, + sessionId: action.sessionId, + actorId: action.actorId, + tenantId: action.tenantId, + channelId: action.channelId, + correlationId: action.correlationId, + agentName: action.agentName, + }), + ) + .digest('hex'); + } + + private isUnexpired(expiresAt: unknown): expiresAt is string { + if (typeof expiresAt !== 'string') return false; + const expiresAtMs = Date.parse(expiresAt); + return Number.isFinite(expiresAtMs) && expiresAtMs > Date.now(); + } + + private isCommandApproval(value: unknown): value is CommandApproval { return ( typeof value === 'object' && value !== null && 'approvalId' in value && 'actionDigest' in value && 'actorId' in value && + 'expiresAt' in value && + 'command' in value + ); + } + + private isRuntimeTerminationApproval(value: unknown): value is RuntimeTerminationApproval { + return ( + typeof value === 'object' && + value !== null && + 'approvalId' in value && + 'actionDigest' in value && + 'actorId' in value && + 'tenantId' in value && + 'providerId' in value && + 'sessionId' in value && + 'channelId' in value && + 'correlationId' in value && + 'agentName' in value && 'expiresAt' in value ); } private key(approvalId: string): string { - return `tess:command-approval:${approvalId}`; + return `interaction:command-approval:${approvalId}`; + } + + private runtimeKey(agentName: string, approvalId: string): string { + return `agent:${encodeURIComponent(agentName)}:command-approval:${approvalId}`; } } diff --git a/apps/gateway/src/commands/commands.module.ts b/apps/gateway/src/commands/commands.module.ts index 0d6c30d..09fb440 100644 --- a/apps/gateway/src/commands/commands.module.ts +++ b/apps/gateway/src/commands/commands.module.ts @@ -6,6 +6,7 @@ import { ReloadModule } from '../reload/reload.module.js'; import { CommandAuthorizationService } from './command-authorization.service.js'; import { CommandExecutorService } from './command-executor.service.js'; import { CommandRegistryService } from './command-registry.service.js'; +import { CommandRuntimeApprovalVerifier } from './runtime-approval-verifier.js'; import { COMMANDS_REDIS } from './commands.tokens.js'; const COMMANDS_QUEUE_HANDLE = 'COMMANDS_QUEUE_HANDLE'; @@ -26,9 +27,15 @@ const COMMANDS_QUEUE_HANDLE = 'COMMANDS_QUEUE_HANDLE'; }, CommandRegistryService, CommandAuthorizationService, + CommandRuntimeApprovalVerifier, + CommandExecutorService, + ], + exports: [ + CommandRegistryService, + CommandAuthorizationService, + CommandRuntimeApprovalVerifier, CommandExecutorService, ], - exports: [CommandRegistryService, CommandExecutorService], }) export class CommandsModule implements OnApplicationShutdown { constructor(@Inject(COMMANDS_QUEUE_HANDLE) private readonly handle: QueueHandle) {} diff --git a/apps/gateway/src/commands/runtime-approval-verifier.ts b/apps/gateway/src/commands/runtime-approval-verifier.ts new file mode 100644 index 0000000..47e0973 --- /dev/null +++ b/apps/gateway/src/commands/runtime-approval-verifier.ts @@ -0,0 +1,23 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { + RuntimeApprovalVerifier, + RuntimeTerminationAction, +} from '../agent/runtime-provider-registry.service.js'; +import { CommandAuthorizationService } from './command-authorization.service.js'; + +/** + * Adapter from the provider registry's exact termination action to the shared, + * Redis-backed `interaction:command-approval:*` store. It has no separate approval + * persistence or replay semantics. + */ +@Injectable() +export class CommandRuntimeApprovalVerifier implements RuntimeApprovalVerifier { + constructor( + @Inject(CommandAuthorizationService) + private readonly authorization: CommandAuthorizationService, + ) {} + + async consume(approvalRef: string, action: RuntimeTerminationAction): Promise { + return this.authorization.consumeRuntimeTerminationApproval(approvalRef, action); + } +} diff --git a/apps/gateway/src/coord/coord.module.ts b/apps/gateway/src/coord/coord.module.ts index d2f46e3..170280d 100644 --- a/apps/gateway/src/coord/coord.module.ts +++ b/apps/gateway/src/coord/coord.module.ts @@ -1,10 +1,31 @@ import { Module } from '@nestjs/common'; +import { InMemoryMosCoordinationPort } from '@mosaicstack/coord'; import { CoordService } from './coord.service.js'; import { CoordController } from './coord.controller.js'; +import { MosCoordinationController } from './mos-coordination.controller.js'; +import { + MOS_COORDINATION_CONFIG, + MOS_COORDINATION_PORT, + MosCoordinationService, +} from './mos-coordination.service.js'; @Module({ - providers: [CoordService], - controllers: [CoordController], - exports: [CoordService], + providers: [ + CoordService, + { + provide: MOS_COORDINATION_PORT, + useFactory: (): InMemoryMosCoordinationPort => new InMemoryMosCoordinationPort(), + }, + { + provide: MOS_COORDINATION_CONFIG, + useFactory: () => ({ + interactionAgentId: process.env['MOSAIC_AGENT_NAME'], + orchestrationAgentId: process.env['MOSAIC_ORCHESTRATOR_AGENT_NAME'], + }), + }, + MosCoordinationService, + ], + controllers: [CoordController, MosCoordinationController], + exports: [CoordService, MosCoordinationService], }) export class CoordModule {} diff --git a/apps/gateway/src/coord/mos-coordination.controller.test.ts b/apps/gateway/src/coord/mos-coordination.controller.test.ts new file mode 100644 index 0000000..0bec4b7 --- /dev/null +++ b/apps/gateway/src/coord/mos-coordination.controller.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi } from 'vitest'; +import { MosCoordinationController } from './mos-coordination.controller.js'; + +const user = { id: 'operator-1', tenantId: 'tenant-a' }; + +describe('MosCoordinationController', () => { + it('derives actor and tenant from the authenticated user rather than handoff input', async () => { + const coordination = { + handoff: vi.fn(async () => ({ handoffId: 'handoff-1' })), + observe: vi.fn(), + result: vi.fn(), + }; + const controller = new MosCoordinationController(coordination as never); + + await controller.handoff({ idempotencyKey: 'request-1', summary: 'Implement' }, user, 'corr-1'); + + expect(coordination.handoff).toHaveBeenCalledWith( + { idempotencyKey: 'request-1', summary: 'Implement' }, + expect.objectContaining({ + actorScope: { userId: 'operator-1', tenantId: 'tenant-a' }, + channelId: 'cli', + correlationId: 'corr-1', + }), + ); + }); + + it('requires a correlation header before invoking the coordination service', async () => { + const coordination = { handoff: vi.fn(), observe: vi.fn(), result: vi.fn() }; + const controller = new MosCoordinationController(coordination as never); + + await expect( + controller.handoff({ idempotencyKey: 'request-1', summary: 'Implement' }, user, undefined), + ).rejects.toThrow('X-Correlation-Id is required'); + expect(coordination.handoff).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/gateway/src/coord/mos-coordination.controller.ts b/apps/gateway/src/coord/mos-coordination.controller.ts new file mode 100644 index 0000000..cacfe7b --- /dev/null +++ b/apps/gateway/src/coord/mos-coordination.controller.ts @@ -0,0 +1,73 @@ +import { + Body, + Controller, + ForbiddenException, + Get, + Headers, + Inject, + Param, + Post, + UseGuards, +} from '@nestjs/common'; +import { AuthGuard } from '../auth/auth.guard.js'; +import { CurrentUser } from '../auth/current-user.decorator.js'; +import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js'; +import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js'; +import type { + MosCoordinationObservationDto, + MosCoordinationResponseDto, + MosCoordinationResultDto, + CreateMosHandoffDto, +} from './mos-coordination.dto.js'; +import { MosCoordinationService } from './mos-coordination.service.js'; + +/** Authenticated interaction-plane boundary for the handoff/observe/result-only Mos contract. */ +@Controller('api/coord/mos') +@UseGuards(AuthGuard) +export class MosCoordinationController { + constructor( + @Inject(MosCoordinationService) private readonly coordination: MosCoordinationService, + ) {} + + @Post('handoff') + async handoff( + @Body() request: CreateMosHandoffDto, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ): Promise { + return { receipt: await this.coordination.handoff(request, this.context(user, correlationId)) }; + } + + @Get(':handoffId/observe') + async observe( + @Param('handoffId') handoffId: string, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ): Promise { + return { + observation: await this.coordination.observe(handoffId, this.context(user, correlationId)), + }; + } + + @Get(':handoffId/result') + async result( + @Param('handoffId') handoffId: string, + @CurrentUser() user: AuthenticatedUserLike, + @Headers('x-correlation-id') correlationId?: string, + ): Promise { + return { result: await this.coordination.result(handoffId, this.context(user, correlationId)) }; + } + + private context( + user: AuthenticatedUserLike, + correlationId?: string, + ): RuntimeProviderRequestContext { + const requestCorrelationId = correlationId?.trim(); + if (!requestCorrelationId) throw new ForbiddenException('X-Correlation-Id is required'); + return { + actorScope: scopeFromUser(user), + channelId: 'cli', + correlationId: requestCorrelationId, + }; + } +} diff --git a/apps/gateway/src/coord/mos-coordination.dto.ts b/apps/gateway/src/coord/mos-coordination.dto.ts new file mode 100644 index 0000000..7967753 --- /dev/null +++ b/apps/gateway/src/coord/mos-coordination.dto.ts @@ -0,0 +1,25 @@ +import type { + CoordinationObservation, + CoordinationResult, + MosHandoffReceipt, +} from '@mosaicstack/coord'; + +/** Input accepted at the gateway coordination boundary. Agent identity is not caller-controlled. */ +export interface CreateMosHandoffDto { + idempotencyKey: string; + summary: string; + context?: string; + missionId?: string; +} + +export interface MosCoordinationResponseDto { + receipt: MosHandoffReceipt; +} + +export interface MosCoordinationObservationDto { + observation: CoordinationObservation; +} + +export interface MosCoordinationResultDto { + result: CoordinationResult; +} diff --git a/apps/gateway/src/coord/mos-coordination.service.test.ts b/apps/gateway/src/coord/mos-coordination.service.test.ts new file mode 100644 index 0000000..51b2a26 --- /dev/null +++ b/apps/gateway/src/coord/mos-coordination.service.test.ts @@ -0,0 +1,217 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + InMemoryMosCoordinationPort, + type MosCoordinationPort, + type MosHandoff, +} from '@mosaicstack/coord'; +import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js'; +import { + MosCoordinationService, + type MosCoordinationConfig, + type MosCoordinationGatewayError, +} from './mos-coordination.service.js'; + +const context: RuntimeProviderRequestContext = { + actorScope: { userId: 'operator-1', tenantId: 'tenant-a' }, + channelId: 'cli', + correlationId: 'corr-1', +}; + +const config: MosCoordinationConfig = { + interactionAgentId: 'Nova', + orchestrationAgentId: 'Conductor', +}; + +function service( + port: MosCoordinationPort = new InMemoryMosCoordinationPort(), + options: { + config?: MosCoordinationConfig; + handoffIdFactory?: () => string; + } = {}, +): MosCoordinationService { + return new MosCoordinationService( + port, + options.config ?? config, + options.handoffIdFactory ?? (() => 'handoff-1'), + ); +} + +describe('MosCoordinationService authority boundary', (): void => { + it('derives identity and actor/tenant scope server-side, then round-trips the native adapter', async (): Promise => { + const adapter = new InMemoryMosCoordinationPort(); + const coordination = service(adapter); + + await expect( + coordination.handoff( + { idempotencyKey: 'request-1', summary: 'Implement the requested feature' }, + context, + ), + ).resolves.toEqual({ + handoffId: 'handoff-1', + targetAgentId: 'Conductor', + status: 'queued', + correlationId: 'corr-1', + }); + + adapter.recordActivity('handoff-1', 'running', 'Mos accepted the request'); + adapter.recordResult('handoff-1', 'completed', 'Merged by Mos'); + + const followUpContext = { ...context, correlationId: 'corr-2' }; + await expect(coordination.observe('handoff-1', followUpContext)).resolves.toMatchObject({ + targetAgentId: 'Conductor', + status: 'completed', + }); + await expect(coordination.result('handoff-1', followUpContext)).resolves.toMatchObject({ + targetAgentId: 'Conductor', + status: 'completed', + summary: 'Merged by Mos', + }); + }); + + it('fails closed without calling a port when the interaction requester is unconfigured', async (): Promise => { + const adapter = new InMemoryMosCoordinationPort(); + const handoff = vi.spyOn(adapter, 'handoff'); + const coordination = service(adapter, { + config: { interactionAgentId: '', orchestrationAgentId: 'Conductor' }, + }); + + await expect( + coordination.handoff( + { idempotencyKey: 'request-1', summary: 'Implement the requested feature' }, + context, + ), + ).rejects.toMatchObject({ + code: 'unconfigured_requester', + } satisfies Partial); + expect(handoff).not.toHaveBeenCalled(); + }); + + it('rejects self-delegation configuration before delivering work', async (): Promise => { + const adapter = new InMemoryMosCoordinationPort(); + const handoff = vi.spyOn(adapter, 'handoff'); + const coordination = service(adapter, { + config: { interactionAgentId: 'Nova', orchestrationAgentId: 'Nova' }, + }); + + await expect( + coordination.handoff( + { idempotencyKey: 'request-1', summary: 'Implement the requested feature' }, + context, + ), + ).rejects.toThrow('Interaction and orchestration identities must differ'); + expect(handoff).not.toHaveBeenCalled(); + }); + + it('denies cross-tenant observe and result before calling the adapter', async (): Promise => { + const adapter = new InMemoryMosCoordinationPort(); + const observe = vi.spyOn(adapter, 'observe'); + const result = vi.spyOn(adapter, 'result'); + const coordination = service(adapter); + await coordination.handoff( + { idempotencyKey: 'request-1', summary: 'Implement the requested feature' }, + context, + ); + + const otherTenant = { + ...context, + actorScope: { ...context.actorScope, tenantId: 'tenant-b' }, + }; + await expect(coordination.observe('handoff-1', otherTenant)).rejects.toMatchObject({ + code: 'cross_tenant_forbidden', + } satisfies Partial); + await expect(coordination.result('handoff-1', otherTenant)).rejects.toMatchObject({ + code: 'cross_tenant_forbidden', + } satisfies Partial); + expect(observe).not.toHaveBeenCalled(); + expect(result).not.toHaveBeenCalled(); + }); + + it('scopes idempotency by actor and joins concurrent retries without duplicate delivery', async (): Promise => { + let handoffSequence = 0; + let release: (() => void) | undefined; + const delivered = new Promise((resolve: () => void): void => { + release = resolve; + }); + const adapter: MosCoordinationPort = { + handoff: vi.fn(async (handoff: MosHandoff) => { + await delivered; + return { + handoffId: handoff.handoffId, + targetAgentId: handoff.targetAgentId, + status: 'queued' as const, + correlationId: handoff.scope.correlationId, + }; + }), + observe: vi.fn(), + result: vi.fn(), + }; + const coordination = service(adapter, { + handoffIdFactory: (): string => `handoff-${++handoffSequence}`, + }); + const request = { idempotencyKey: 'request-1', summary: 'Implement the requested feature' }; + + const first = coordination.handoff(request, context); + const retry = coordination.handoff(request, context); + expect(adapter.handoff).toHaveBeenCalledTimes(1); + release?.(); + await expect(Promise.all([first, retry])).resolves.toEqual([ + expect.objectContaining({ handoffId: 'handoff-1' }), + expect.objectContaining({ handoffId: 'handoff-1' }), + ]); + + await expect( + coordination.handoff(request, { + ...context, + actorScope: { ...context.actorScope, userId: 'operator-2' }, + }), + ).resolves.toMatchObject({ handoffId: 'handoff-2' }); + expect(adapter.handoff).toHaveBeenCalledTimes(2); + }); + + it('rejects idempotency-key payload drift and malformed handoff input before delivery', async (): Promise => { + const adapter = new InMemoryMosCoordinationPort(); + const handoff = vi.spyOn(adapter, 'handoff'); + const coordination = service(adapter); + await coordination.handoff( + { idempotencyKey: 'request-1', summary: 'Implement the requested feature' }, + context, + ); + + await expect( + coordination.handoff({ idempotencyKey: 'request-1', summary: 'Different work' }, context), + ).rejects.toMatchObject({ + code: 'handoff_conflict', + } satisfies Partial); + await expect( + coordination.handoff({ idempotencyKey: 'request-2', summary: '' }, context), + ).rejects.toMatchObject({ + code: 'invalid_request', + } satisfies Partial); + await expect( + coordination.handoff({ idempotencyKey: 'request-3', summary: 'x'.repeat(2_049) }, context), + ).rejects.toMatchObject({ + code: 'invalid_request', + } satisfies Partial); + expect(handoff).toHaveBeenCalledTimes(1); + }); + + it('fails closed when the port reports a target that drifts from configuration', async (): Promise => { + const adapter: MosCoordinationPort = { + handoff: vi.fn(async (handoff: MosHandoff) => ({ + handoffId: handoff.handoffId, + targetAgentId: 'Unexpected', + status: 'accepted' as const, + correlationId: handoff.scope.correlationId, + })), + observe: vi.fn(), + result: vi.fn(), + }; + + await expect( + service(adapter).handoff( + { idempotencyKey: 'request-1', summary: 'Implement the requested feature' }, + context, + ), + ).rejects.toMatchObject({ code: 'target_drift' }); + }); +}); diff --git a/apps/gateway/src/coord/mos-coordination.service.ts b/apps/gateway/src/coord/mos-coordination.service.ts new file mode 100644 index 0000000..bc4bb98 --- /dev/null +++ b/apps/gateway/src/coord/mos-coordination.service.ts @@ -0,0 +1,300 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + MosCoordinationClient, + type CoordinationObservation, + type CoordinationResult, + type CoordinationScope, + type MosCoordinationIdentity, + type MosCoordinationPort, + type MosHandoffReceipt, +} from '@mosaicstack/coord'; +import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js'; +import type { CreateMosHandoffDto } from './mos-coordination.dto.js'; + +export const MOS_COORDINATION_PORT = Symbol('MOS_COORDINATION_PORT'); +export const MOS_COORDINATION_CONFIG = Symbol('MOS_COORDINATION_CONFIG'); + +const HANDOFF_TRACKING_TTL_MS = 60 * 60 * 1_000; +const MAX_TRACKED_HANDOFFS = 1_000; +const MAX_IDEMPOTENCY_KEY_LENGTH = 128; +const MAX_SUMMARY_LENGTH = 2_048; +const MAX_CONTEXT_LENGTH = 8_192; +const MAX_MISSION_ID_LENGTH = 128; + +export interface MosCoordinationConfig { + interactionAgentId?: string; + orchestrationAgentId?: string; +} + +interface HandoffOwner { + actorId: string; + tenantId: string; + requesterAgentId: string; + correlationId: string; + expiresAt: number; +} + +interface NormalizedMosHandoffRequest { + idempotencyKey: string; + summary: string; + context?: string; + missionId?: string; +} + +interface TrackedHandoff { + request: NormalizedMosHandoffRequest; + receipt: Promise; + expiresAt: number; +} + +/** + * Gateway authority boundary for the interaction agent. It derives requester, + * actor, and tenant from trusted server configuration and authentication; no + * channel request can name a target or gain Mos-owned orchestration verbs. + */ +@Injectable() +export class MosCoordinationService { + private readonly owners = new Map(); + private readonly handoffsByIdempotencyKey = new Map(); + + constructor( + @Inject(MOS_COORDINATION_PORT) private readonly port: MosCoordinationPort, + @Inject(MOS_COORDINATION_CONFIG) private readonly config: MosCoordinationConfig, + private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(), + ) {} + + async handoff( + request: CreateMosHandoffDto, + context: RuntimeProviderRequestContext, + ): Promise { + this.pruneExpiredTracking(); + const normalized = this.normalizeRequest(request); + const scope = this.scope(context); + const idempotencyKey = this.idempotencyKey(normalized.idempotencyKey, scope); + const existing = this.handoffsByIdempotencyKey.get(idempotencyKey); + if (existing !== undefined) { + if (!sameRequest(existing.request, normalized)) { + throw new MosCoordinationGatewayError( + 'handoff_conflict', + 'Mos handoff idempotency key is already bound to different immutable input', + ); + } + return existing.receipt; + } + + const pending = this.deliverHandoff(this.handoffIdFactory(), normalized, scope); + const tracked: TrackedHandoff = { + request: normalized, + receipt: pending, + expiresAt: this.expiresAt(), + }; + this.handoffsByIdempotencyKey.set(idempotencyKey, tracked); + this.enforceTrackingLimit(this.handoffsByIdempotencyKey); + try { + return await pending; + } catch (error: unknown) { + if (this.handoffsByIdempotencyKey.get(idempotencyKey) === tracked) { + this.handoffsByIdempotencyKey.delete(idempotencyKey); + } + throw error; + } + } + + async observe( + handoffId: string, + context: RuntimeProviderRequestContext, + ): Promise { + this.pruneExpiredTracking(); + const scope = this.scope(context); + const owner = this.ownerFor(handoffId, scope); + return this.client().observe(handoffId, { ...scope, correlationId: owner.correlationId }); + } + + async result( + handoffId: string, + context: RuntimeProviderRequestContext, + ): Promise { + this.pruneExpiredTracking(); + const scope = this.scope(context); + const owner = this.ownerFor(handoffId, scope); + return this.client().result(handoffId, { ...scope, correlationId: owner.correlationId }); + } + + private async deliverHandoff( + handoffId: string, + request: NormalizedMosHandoffRequest, + scope: CoordinationScope, + ): Promise { + const receipt = await this.client((): string => handoffId).handoff(request, scope); + const owner: HandoffOwner = { + actorId: scope.actorId, + tenantId: scope.tenantId, + requesterAgentId: scope.requesterAgentId, + correlationId: scope.correlationId, + expiresAt: this.expiresAt(), + }; + const existing = this.owners.get(receipt.handoffId); + if (existing !== undefined && !sameOwner(existing, owner)) { + throw new MosCoordinationGatewayError( + 'handoff_conflict', + 'Mos handoff ID is already bound to a different authenticated scope', + ); + } + this.owners.set(receipt.handoffId, owner); + this.enforceTrackingLimit(this.owners); + return receipt; + } + + private client(handoffIdFactory?: () => string): MosCoordinationClient { + return new MosCoordinationClient(this.identity(), this.port, handoffIdFactory); + } + + private identity(): MosCoordinationIdentity { + const interactionAgentId = this.config.interactionAgentId?.trim(); + const orchestrationAgentId = this.config.orchestrationAgentId?.trim(); + if (!interactionAgentId) { + throw new MosCoordinationGatewayError( + 'unconfigured_requester', + 'Interaction agent identity is not configured', + ); + } + if (!orchestrationAgentId) { + throw new MosCoordinationGatewayError( + 'unconfigured_target', + 'Orchestration agent identity is not configured', + ); + } + return { interactionAgentId, orchestrationAgentId }; + } + + private scope(context: RuntimeProviderRequestContext): CoordinationScope { + const identity = this.identity(); + return Object.freeze({ + actorId: context.actorScope.userId, + tenantId: context.actorScope.tenantId, + correlationId: context.correlationId, + requesterAgentId: identity.interactionAgentId, + }); + } + + private normalizeRequest(request: CreateMosHandoffDto): NormalizedMosHandoffRequest { + if (typeof request !== 'object' || request === null) { + throw new MosCoordinationGatewayError('invalid_request', 'Mos handoff request is invalid'); + } + const idempotencyKey = this.requiredString( + request.idempotencyKey, + 'idempotency key', + MAX_IDEMPOTENCY_KEY_LENGTH, + ); + const summary = this.requiredString(request.summary, 'summary', MAX_SUMMARY_LENGTH); + const context = this.optionalString(request.context, 'context', MAX_CONTEXT_LENGTH); + const missionId = this.optionalString(request.missionId, 'mission ID', MAX_MISSION_ID_LENGTH); + return Object.freeze({ + idempotencyKey, + summary, + ...(context === undefined ? {} : { context }), + ...(missionId === undefined ? {} : { missionId }), + }); + } + + private idempotencyKey(requestKey: string, scope: CoordinationScope): string { + return `${scope.tenantId}\u0000${scope.actorId}\u0000${scope.requesterAgentId}\u0000${requestKey}`; + } + + private requiredString(value: unknown, field: string, maximumLength: number): string { + if (typeof value !== 'string') { + throw new MosCoordinationGatewayError( + 'invalid_request', + `Mos handoff ${field} must be a string`, + ); + } + const normalized = value.trim(); + if (normalized.length === 0 || normalized.length > maximumLength) { + throw new MosCoordinationGatewayError('invalid_request', `Mos handoff ${field} is invalid`); + } + return normalized; + } + + private optionalString(value: unknown, field: string, maximumLength: number): string | undefined { + if (value === undefined) return undefined; + return this.requiredString(value, field, maximumLength); + } + + private expiresAt(): number { + return Date.now() + HANDOFF_TRACKING_TTL_MS; + } + + private pruneExpiredTracking(): void { + const now = Date.now(); + for (const [key, tracked] of this.handoffsByIdempotencyKey) { + if (tracked.expiresAt <= now) this.handoffsByIdempotencyKey.delete(key); + } + for (const [key, owner] of this.owners) { + if (owner.expiresAt <= now) this.owners.delete(key); + } + } + + private enforceTrackingLimit(entries: Map): void { + while (entries.size > MAX_TRACKED_HANDOFFS) { + const oldest = entries.keys().next().value; + if (typeof oldest !== 'string') return; + entries.delete(oldest); + } + } + + private ownerFor(handoffId: string, scope: CoordinationScope): HandoffOwner { + const owner = this.owners.get(handoffId); + if (owner === undefined) { + throw new MosCoordinationGatewayError('not_found', 'Mos handoff was not found'); + } + if ( + owner.tenantId !== scope.tenantId || + owner.actorId !== scope.actorId || + owner.requesterAgentId !== scope.requesterAgentId + ) { + throw new MosCoordinationGatewayError( + 'cross_tenant_forbidden', + 'Mos handoff is outside the authenticated scope', + ); + } + return owner; + } +} + +export type MosCoordinationGatewayErrorCode = + | 'cross_tenant_forbidden' + | 'handoff_conflict' + | 'invalid_request' + | 'not_found' + | 'unconfigured_requester' + | 'unconfigured_target'; + +function sameOwner(left: HandoffOwner, right: HandoffOwner): boolean { + return ( + left.actorId === right.actorId && + left.tenantId === right.tenantId && + left.requesterAgentId === right.requesterAgentId + ); +} + +function sameRequest( + left: NormalizedMosHandoffRequest, + right: NormalizedMosHandoffRequest, +): boolean { + return ( + left.idempotencyKey === right.idempotencyKey && + left.summary === right.summary && + left.context === right.context && + left.missionId === right.missionId + ); +} + +export class MosCoordinationGatewayError extends Error { + constructor( + readonly code: MosCoordinationGatewayErrorCode, + message: string, + ) { + super(message); + this.name = MosCoordinationGatewayError.name; + } +} diff --git a/apps/gateway/src/gc/session-gc.service.spec.ts b/apps/gateway/src/gc/session-gc.service.spec.ts index c6f3948..8c5ab6a 100644 --- a/apps/gateway/src/gc/session-gc.service.spec.ts +++ b/apps/gateway/src/gc/session-gc.service.spec.ts @@ -108,7 +108,7 @@ describe('SessionGCService', () => { } as never; const payload = { command: 'gc', conversationId: 'owned' }; const approval = await authorization.createApproval(command, payload, 'admin-1'); - const approvalKey = `tess:command-approval:${approval!.approvalId}`; + const approvalKey = `interaction:command-approval:${approval!.approvalId}`; const gc = new SessionGCService(redis as never, mockLogService as unknown as LogService); await gc.collect('owned'); diff --git a/apps/gateway/src/memory/memory.module.ts b/apps/gateway/src/memory/memory.module.ts index 779ad40..56e0278 100644 --- a/apps/gateway/src/memory/memory.module.ts +++ b/apps/gateway/src/memory/memory.module.ts @@ -3,8 +3,10 @@ import { createMemory, type Memory, createMemoryAdapter, + createOperatorMemoryPlugin, type MemoryAdapter, type MemoryConfig, + type OperatorMemoryPlugin, } from '@mosaicstack/memory'; import type { Db } from '@mosaicstack/db'; import type { StorageAdapter } from '@mosaicstack/storage'; @@ -14,6 +16,9 @@ import { DB, STORAGE_ADAPTER } from '../database/database.module.js'; import { MEMORY } from './memory.tokens.js'; import { MemoryController } from './memory.controller.js'; import { EmbeddingService } from './embedding.service.js'; +import { redactSensitiveContent } from '@mosaicstack/log'; + +export const OPERATOR_MEMORY_PLUGIN = 'OPERATOR_MEMORY_PLUGIN'; export const MEMORY_ADAPTER = 'MEMORY_ADAPTER'; @@ -38,9 +43,24 @@ function buildMemoryConfig(config: MosaicConfig, storageAdapter: StorageAdapter) createMemoryAdapter(buildMemoryConfig(config, storageAdapter)), inject: [MOSAIC_CONFIG, STORAGE_ADAPTER], }, + { + provide: OPERATOR_MEMORY_PLUGIN, + useFactory: (adapter: MemoryAdapter): OperatorMemoryPlugin | null => { + const instanceId = process.env['MOSAIC_OPERATOR_MEMORY_INSTANCE_ID']?.trim(); + const namespace = process.env['MOSAIC_OPERATOR_MEMORY_NAMESPACE']?.trim(); + if (!instanceId || !namespace) return null; + return createOperatorMemoryPlugin({ + adapter, + instanceId, + namespace, + redact: (content) => redactSensitiveContent(content).content, + }); + }, + inject: [MEMORY_ADAPTER], + }, EmbeddingService, ], controllers: [MemoryController], - exports: [MEMORY, MEMORY_ADAPTER, EmbeddingService], + exports: [MEMORY, MEMORY_ADAPTER, OPERATOR_MEMORY_PLUGIN, EmbeddingService], }) export class MemoryModule {} diff --git a/apps/gateway/src/plugin/discord-ingress.security.spec.ts b/apps/gateway/src/plugin/discord-ingress.security.spec.ts index 0530170..0c8bb46 100644 --- a/apps/gateway/src/plugin/discord-ingress.security.spec.ts +++ b/apps/gateway/src/plugin/discord-ingress.security.spec.ts @@ -1,13 +1,138 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { createDiscordIngressEnvelope, verifyDiscordIngressEnvelope, + DiscordPlugin, type DiscordIngressPayload, + parseDiscordInteractionBindings, + resolveDiscordInteractionActorId, + resolveDiscordInteractionBinding, } from '@mosaicstack/discord-plugin'; +import { RuntimeProviderService } from '../agent/runtime-provider-registry.service.js'; +import { ChatGateway } from '../chat/chat.gateway.js'; +import { CommandAuthorizationService } from '../commands/command-authorization.service.js'; import { validateDiscordServiceToken } from '../chat/chat.gateway-auth.js'; import { DiscordReplayProtector } from './discord-replay-protector.js'; const SERVICE_TOKEN = 'test-service-token'; +const ENV_KEYS = [ + 'DISCORD_SERVICE_TOKEN', + 'DISCORD_SERVICE_USER_ID', + 'DISCORD_SERVICE_TENANT_ID', + 'DISCORD_INTERACTION_BINDINGS', + 'DISCORD_ALLOWED_GUILD_IDS', + 'DISCORD_ALLOWED_CHANNEL_IDS', + 'DISCORD_ALLOWED_USER_IDS', + 'MOSAIC_AGENT_NAME', +] as const; +const savedEnv = new Map(); + +function configureDiscordEnv(role: 'admin' | 'member' = 'admin'): void { + for (const key of ENV_KEYS) savedEnv.set(key, process.env[key]); + process.env['DISCORD_SERVICE_TOKEN'] = SERVICE_TOKEN; + process.env['DISCORD_SERVICE_USER_ID'] = 'discord-service'; + process.env['DISCORD_SERVICE_TENANT_ID'] = 'tenant-discord'; + process.env['MOSAIC_AGENT_NAME'] = 'Nova'; + process.env['DISCORD_ALLOWED_GUILD_IDS'] = 'guild-001'; + process.env['DISCORD_ALLOWED_CHANNEL_IDS'] = 'channel-001'; + process.env['DISCORD_ALLOWED_USER_IDS'] = 'user-001'; + process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([ + { + instanceId: 'Nova', + guildId: 'guild-001', + channelId: 'channel-001', + pairedUsers: { + 'user-001': { + role: role === 'admin' ? 'admin' : 'operator', + mosaicUserId: 'mosaic-admin-001', + }, + }, + }, + ]); +} + +afterEach((): void => { + for (const key of ENV_KEYS) { + const value = savedEnv.get(key); + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + savedEnv.clear(); +}); + +function commandAuthorization(role: 'admin' | 'member'): CommandAuthorizationService { + const entries = new Map(); + const db = { + select: () => ({ from: () => ({ where: () => ({ limit: async () => [{ role }] }) }) }), + }; + const redis = { + get: async (key: string) => entries.get(key) ?? null, + set: async (key: string, value: string) => entries.set(key, value), + del: async (key: string) => Number(entries.delete(key)), + }; + return new CommandAuthorizationService(db as never, redis); +} + +function discordGateway(role: 'admin' | 'member'): { + gateway: ChatGateway; + client: { data: { discordService: boolean }; emit: ReturnType }; + consumedActions: Array<{ actorId: string; correlationId: string }>; + durable: { getSnapshot: ReturnType }; + audit: { record: ReturnType }; +} { + const authorization = commandAuthorization(role); + const consumedActions: Array<{ actorId: string; correlationId: string }> = []; + const durable = { + getSnapshot: vi.fn().mockResolvedValue({ + identity: { agentName: 'Nova', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, + }), + }; + const audit = { record: vi.fn().mockResolvedValue(undefined) }; + const runtimeRegistry = new RuntimeProviderService( + { + require: () => ({ + capabilities: async () => ({ supported: ['session.terminate'] }), + terminate: async () => undefined, + }), + } as never, + { record: async () => undefined } as never, + { + consume: async (approvalId, action) => { + consumedActions.push({ actorId: action.actorId, correlationId: action.correlationId }); + return authorization.consumeRuntimeTerminationApproval(approvalId, action); + }, + }, + ); + return { + gateway: new ChatGateway( + {} as never, + {} as never, + {} as never, + {} as never, + {} as never, + {} as never, + authorization, + runtimeRegistry, + durable as never, + audit as never, + ), + client: { data: { discordService: true }, emit: vi.fn() }, + consumedActions, + durable, + audit, + }; +} + +function ingressEnvelope( + content: string, + messageId: string, + overrides: Partial = {}, +): ReturnType { + return createDiscordIngressEnvelope( + createPayload({ content, messageId, ...overrides }), + SERVICE_TOKEN, + ); +} function createPayload(overrides: Partial = {}): DiscordIngressPayload { return { @@ -23,6 +148,42 @@ function createPayload(overrides: Partial = {}): DiscordI } describe('Discord ingress security', () => { + it('keeps legacy role-only bindings valid while withholding privileged actor identity', () => { + const [binding] = parseDiscordInteractionBindings( + JSON.stringify([ + { + instanceId: 'Nova', + guildId: 'guild-001', + channelId: 'channel-001', + pairedUsers: { 'user-001': 'admin' }, + }, + ]), + ); + expect( + resolveDiscordInteractionBinding([binding!], 'guild-001', 'channel-001', 'user-001', 'send'), + ).toEqual(binding); + expect(resolveDiscordInteractionActorId(binding!, 'user-001')).toBeNull(); + }); + + it('binds a differently named configured interaction instance without code changes', () => { + const binding = resolveDiscordInteractionBinding( + [ + { + instanceId: 'Nova', + guildId: 'guild-001', + channelId: 'channel-001', + pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' } }, + }, + ], + 'guild-001', + 'channel-001', + 'user-001', + 'send', + ); + + expect(binding?.instanceId).toBe('Nova'); + }); + it('accepts only the configured Discord service identity', () => { expect(validateDiscordServiceToken(SERVICE_TOKEN, SERVICE_TOKEN)).toBe(true); expect(validateDiscordServiceToken('wrong-service-token', SERVICE_TOKEN)).toBe(false); @@ -86,4 +247,197 @@ describe('Discord ingress security', () => { expect(replayProtector.claim('discord-message-003')).toBe(true); expect(replayProtector.size).toBe(2); }); + + it('consumes the exact target once when approval and stop are separate Discord messages', async () => { + configureDiscordEnv(); + const { gateway, client, consumedActions } = discordGateway('admin'); + await gateway.handleDiscordApproval( + client as never, + ingressEnvelope('/approve', 'approve-message', { + correlationId: 'approval-ingress-correlation', + }), + ); + const approval = client.emit.mock.calls.find( + ([event]) => event === 'discord:approval', + )?.[1] as { + approvalId: string; + success: boolean; + }; + expect(approval.success).toBe(true); + + await gateway.handleDiscordStop( + client as never, + ingressEnvelope(`/stop ${approval.approvalId}`, 'stop-message', { + correlationId: 'stop-ingress-correlation', + }), + ); + expect(client.emit).toHaveBeenCalledWith('discord:stop', { + correlationId: 'stop-ingress-correlation', + success: true, + }); + expect(consumedActions).toEqual([ + { + actorId: 'mosaic-admin-001', + correlationId: expect.stringMatching(/^discord-action:v1:/), + }, + ]); + }); + + it('audits a Discord mint-side authorization denial', async () => { + configureDiscordEnv(); + const { gateway, client, audit } = discordGateway('member'); + + await gateway.handleDiscordApproval( + client as never, + ingressEnvelope('/approve', 'denied-approve'), + ); + + expect(client.emit).toHaveBeenCalledWith('discord:approval', { + correlationId: 'correlation-001', + success: false, + approvalId: undefined, + expiresAt: undefined, + }); + expect(audit.record).toHaveBeenCalledWith( + expect.objectContaining({ + outcome: 'denied', + operation: 'session.terminate', + errorCode: 'policy_denied', + }), + ); + }); + + it.each([ + [ + 'binding', + () => { + process.env['MOSAIC_AGENT_NAME'] = 'Other'; + }, + ], + [ + 'durable session', + (durable: { getSnapshot: ReturnType }) => { + durable.getSnapshot.mockResolvedValueOnce({ + identity: { agentName: 'Other', providerId: 'fleet', runtimeSessionId: 'runtime-1' }, + }); + }, + ], + ])( + 'rejects approval when the %s targets a different runtime agent', + async (_source, configure) => { + configureDiscordEnv(); + const { gateway, client, durable } = discordGateway('admin'); + configure(durable); + + await gateway.handleDiscordApproval( + client as never, + ingressEnvelope('/approve', 'mismatched-agent-approve'), + ); + + expect(client.emit).toHaveBeenCalledWith('discord:approval', { + correlationId: 'correlation-001', + success: false, + approvalId: undefined, + expiresAt: undefined, + }); + }, + ); + + it('rejects unpaired and non-admin Discord users for approval and stop', async () => { + configureDiscordEnv(); + const { gateway, client } = discordGateway('member'); + await gateway.handleDiscordApproval( + client as never, + ingressEnvelope('/approve', 'member-approve'), + ); + expect(client.emit).toHaveBeenCalledWith('discord:approval', { + correlationId: 'correlation-001', + success: false, + approvalId: undefined, + expiresAt: undefined, + }); + + process.env['DISCORD_INTERACTION_BINDINGS'] = JSON.stringify([]); + await gateway.handleDiscordStop( + client as never, + ingressEnvelope('/stop forged', 'unpaired-stop'), + ); + expect(client.emit).not.toHaveBeenCalledWith('discord:stop', expect.anything()); + }); + + it('rejects replaying a Discord-created termination approval', async () => { + configureDiscordEnv(); + const { gateway, client } = discordGateway('admin'); + await gateway.handleDiscordApproval( + client as never, + ingressEnvelope('/approve', 'replay-approve', { + correlationId: 'replay-approval-correlation', + }), + ); + const approval = client.emit.mock.calls.find( + ([event]) => event === 'discord:approval', + )?.[1] as { + approvalId: string; + }; + await gateway.handleDiscordStop( + client as never, + ingressEnvelope(`/stop ${approval.approvalId}`, 'replay-stop-one', { + correlationId: 'replay-stop-correlation-one', + }), + ); + await gateway.handleDiscordStop( + client as never, + ingressEnvelope(`/stop ${approval.approvalId}`, 'replay-stop-two', { + correlationId: 'replay-stop-correlation-two', + }), + ); + const stopResults = client.emit.mock.calls.filter(([event]) => event === 'discord:stop'); + expect(stopResults.map(([, result]) => (result as { success: boolean }).success)).toEqual([ + true, + false, + ]); + }); + + it('accepts a thread message through its allowed bound parent channel', () => { + const emitted = vi.fn(); + const plugin = new DiscordPlugin({ + token: 'unused', + gatewayUrl: 'http://unused', + serviceToken: SERVICE_TOKEN, + allowedGuildIds: ['guild-001'], + allowedChannelIds: ['channel-001'], + allowedUserIds: ['user-001'], + interactionBindings: [ + { + instanceId: 'Nova', + guildId: 'guild-001', + channelId: 'channel-001', + pairedUsers: { 'user-001': { role: 'operator', mosaicUserId: 'mosaic-operator-001' } }, + }, + ], + }); + const internals = plugin as unknown as { + client: { user: { id: string } }; + socket: { connected: boolean; emit: ReturnType }; + handleDiscordMessage(message: unknown): void; + }; + internals.client = { user: { id: 'bot-001' } }; + internals.socket = { connected: true, emit: emitted }; + internals.handleDiscordMessage({ + id: 'thread-message', + guildId: 'guild-001', + channelId: 'thread-001', + author: { id: 'user-001', bot: false }, + mentions: { has: () => true }, + content: '<@bot-001> hello from thread', + channel: { parentId: 'channel-001' }, + attachments: new Map(), + }); + + const [, envelope] = emitted.mock.calls[0] as [ + string, + ReturnType, + ]; + expect(verifyDiscordIngressEnvelope(envelope, SERVICE_TOKEN)?.channelId).toBe('channel-001'); + }); }); diff --git a/apps/gateway/src/plugin/plugin.module.ts b/apps/gateway/src/plugin/plugin.module.ts index dc059ef..fdf13cf 100644 --- a/apps/gateway/src/plugin/plugin.module.ts +++ b/apps/gateway/src/plugin/plugin.module.ts @@ -6,7 +6,7 @@ import { type OnModuleDestroy, type OnModuleInit, } from '@nestjs/common'; -import { DiscordPlugin } from '@mosaicstack/discord-plugin'; +import { DiscordPlugin, parseDiscordInteractionBindings } from '@mosaicstack/discord-plugin'; import { TelegramPlugin } from '@mosaicstack/telegram-plugin'; import { PluginService } from './plugin.service.js'; import type { IChannelPlugin } from './plugin.interface.js'; @@ -85,6 +85,9 @@ function createPluginRegistry(): IChannelPlugin[] { allowedGuildIds: requiredDiscordAllowlist('DISCORD_ALLOWED_GUILD_IDS'), allowedChannelIds: requiredDiscordAllowlist('DISCORD_ALLOWED_CHANNEL_IDS'), allowedUserIds: requiredDiscordAllowlist('DISCORD_ALLOWED_USER_IDS'), + interactionBindings: parseDiscordInteractionBindings( + process.env['DISCORD_INTERACTION_BINDINGS'], + ), }), ), ); diff --git a/docs/scratchpads/tess-20260712.md b/docs/scratchpads/tess-20260712.md index 819d5c5..5f21ca9 100644 --- a/docs/scratchpads/tess-20260712.md +++ b/docs/scratchpads/tess-20260712.md @@ -56,3 +56,11 @@ **Final focused review:** PASS. Deterministic audit validated all task repository roots with zero missing paths; no planning placeholders remained; security prerequisites still gate Tess exposure; observability traceability is explicit. **Current gate:** planning PR must merge to `main` with terminal-green CI before any source-code worker starts. + +## 2026-07-13 — M3 cross-surface delivery + +**Branch:** `feat/tess-m3-integration` from `main` at `84d884b9`. + +**Delivered:** Stable Discord `conversationId` enrollment after a visible provider/runtime session is known; idempotent provider-session rebinding that preserves agent/tenant/owner scope; Discord approval/stop target resolution through the durable snapshot; SSE runtime streaming after CLI attach; denial/audit parity including provider authorization denials and HTTP 403 approval-denial mapping. + +**Evidence:** Gateway targeted suite: 37 tests passed; Mosaic CLI interaction test passed; agent durable-session test passed; gateway and CLI typechecks passed; changed-file format and whitespace checks passed. Codex security review found no confident vulnerability. Code review identified a Fastify exception-response mismatch and two UX/acknowledgement issues; all were corrected before the final validation run. diff --git a/docs/scratchpads/tess-m2-001-pi-service.md b/docs/scratchpads/tess-m2-001-pi-service.md new file mode 100644 index 0000000..16aaffd --- /dev/null +++ b/docs/scratchpads/tess-m2-001-pi-service.md @@ -0,0 +1,40 @@ +# TESS-M2-001 — Pi Interaction Service + +## Scope + +- Add a generic rostered/systemd Pi operator-interaction service in + `packages/mosaic/framework`. +- Pin the service to `openai/gpt-5.6-sol`, high reasoning, and the + `operator-interaction` tool policy. +- Keep identity as provisioning data; the product name appears only in the + committed example roster. + +## Security and Configuration Invariants + +1. The chosen display/roster name is supplied as data and must exactly match the + generic systemd instance. +2. The service fails before launch if runtime, model, reasoning, or tool policy + differs from the pinned policy. +3. Effective-policy output includes only name, runtime, model, reasoning, and + tool policy; it does not inspect or output credential variables. +4. The default example is replaceable without a source change; the TDD suite + provisions `Nova` from the same profile. + +## Evidence + +- `src/fleet/tess-service-profile.test.ts` proves a `Nova` provisioning path, + roster parser/env serialization, effective-policy output, fail-fast drift + rejection, and absence of the product name from generic source/profile. +- `test-fleet-units.sh` validates the generic interaction systemd unit requires + per-agent config and invokes fail-fast startup validation. +- `test-start-agent-session.sh` proves the tool-policy value is exported into + the Pi pane; `compose-contract.spec.ts` proves it becomes an explicit + runtime contract block. +- Fresh-worktree dependency install plus root `pnpm typecheck`, `pnpm lint`, + `pnpm format:check`, and `pnpm test` passed; package/full fleet suites passed. +- Independent Codex code and security reviews passed with no remaining findings. + +## Delivery Notes + +- Branch starts from fresh `origin/main` at `86a50138`. +- PR targets `main` and references issue `#708`. diff --git a/docs/scratchpads/tess-m2-002-durable-state.md b/docs/scratchpads/tess-m2-002-durable-state.md new file mode 100644 index 0000000..c2e3b80 --- /dev/null +++ b/docs/scratchpads/tess-m2-002-durable-state.md @@ -0,0 +1,63 @@ +# TESS-M2-002 — Durable Tess State + +- **Issue:** #708 +- **Task:** `TESS-M2-002` / `TESS-STA-001`, `TESS-SEC-007..008` +- **Branch:** `feat/tess-durable-state` +- **Base:** fresh `origin/main` at `e3b5113be21e51d015fa1ae54572929b2a4acd9f` +- **Budget assumption:** 38K task estimate; no explicit cap. Use focused TDD plus workspace validation. + +## Objective + +Persist a Tess session's immutable identity, inbox/outbox idempotency state, checkpoints, +handoffs, and approval bindings so a new service instance can recover it after a process +restart or context compaction without replaying a completed message or applied side effect. + +## Plan + +1. Write recovery/idempotency tests first in `packages/agent/src/tess-durable-session.test.ts`. +2. Add transport-neutral durable-state contracts/state machine in `packages/agent`. +3. Add canonical PostgreSQL schema/migration and a gateway Drizzle repository adapter. +4. Wire gateway service/module and reuse `tess:command-approval:*` durable approval semantics + for exact, actor/tenant/action-bound approval consumption. +5. Test PGlite restart recovery with separate service instances sharing the same durable DB. +6. Document the recovery/compaction operation and update Tess architecture evidence. +7. Run focused, cold-cache, workspace, migration, review, commit, push, and open PR to `main`. + +## Required Evidence + +| Requirement | Primary evidence | +| --- | --- | +| Restart recovery | Test creates a second coordinator over unchanged durable store after simulated process death. | +| No duplicate side effects | Duplicate ingress and post-restart dispatch assert one handler/effect invocation. | +| Compaction survival | Checkpoint/handoff/reconstructed state preserve the same session identity and pending records. | +| Durable approvals | Existing `tess:command-approval` record is consumed only once and survives a new authorization service instance. | +| Handoff | Stored handoff is portable and reconstructed without live process state. | + +## Progress + +- Intake complete: PRD AC-TESS-06, threat TM-07/TM-08, and verification matrix reviewed. +- Affected surfaces: `packages/agent`, `apps/gateway`, `packages/db`; auth/authorization and DB migration tests required. +- TDD is required (security authorization and critical state mutation). + +## Risks + +- An external provider action cannot be atomically committed with the database. The outbox + gives the receiver a stable idempotency key; generic recovery never replays an ambiguous + `processing` effect, and completed effects are never redispatched. +- PostgreSQL is canonical; PGlite is the local/restart test implementation. + +## Verification + +- TDD red: `pnpm --filter @mosaicstack/agent test src/tess-durable-session.test.ts` + initially failed because the durable-state module did not exist. +- Focused green: 7 agent state-machine tests; 6 PGlite repository tests (including + close/reopen recovery and encrypted-at-rest redaction); 5 durable-approval tests; DB migration tests. +- Full cold-cache green: `pnpm turbo run typecheck lint test --force` completed + 88 tasks with zero cache hits; `pnpm format:check` and `git diff --check` passed. +- Fresh worktree dependency install passed with + `pnpm install --frozen-lockfile --store-dir /home/jarvis/.local/share/pnpm/store`. + The default pnpm store path was inaccessible to this harness, so the explicit + user-owned store path was required. +- Codex review identified plaintext durable payload risk; resolved by AES-256-GCM sealing + after redaction, with an at-rest ciphertext assertion in the PGlite suite. +- Pending final clean review, commit, and PR. diff --git a/docs/scratchpads/tess-m4-001-mos-coordination.md b/docs/scratchpads/tess-m4-001-mos-coordination.md new file mode 100644 index 0000000..f224605 --- /dev/null +++ b/docs/scratchpads/tess-m4-001-mos-coordination.md @@ -0,0 +1,46 @@ +# TESS-M4-001 — Mos Coordination + +- **Issue/task:** #710 / TESS-M4-001 +- **Branch/base:** `feat/tess-mos-coordination` rebased onto `origin/main` `f1c6b37b` +- **Budget assumption:** task estimate 25K; design-first and TDD, with package contract plus gateway boundary only. + +## Objective + +Implement a transport-neutral coordination contract allowing a configured interaction agent to hand off Mos-owned work, observe activity, and receive results while preventing it from gaining coding/general orchestration authority. + +## Plan + +1. Document the contract and enforcement-point sketch; request Mos's decision on the initial concrete transport. +2. Add `@mosaicstack/coord` typed handoff/observe/result contracts and denial errors. +3. Add a gateway service which derives actor/tenant/requester identity from trusted context/configuration and validates authority. +4. Add contract and gateway boundary tests for configurable identities, self-delegation, target drift, and cross-tenant read denial. +5. Run focused, cold-cache, baseline tests; independent review; PR lifecycle. + +## Design checkpoint — 2026-07-12 + +Created `docs/tess/MOS-COORDINATION.md`. Mos approved the design and selected the native in-process `InMemoryMosCoordinationPort` for M4. Fleet/tmux remains a documented M5 adapter seam; no Mos-side consumer is built in this task. + +## Progress checkpoint — 2026-07-13 + +- Implemented `MosCoordinationPort` with handoff/observe/result only, an authority-checking client, and deterministic native adapter in `@mosaicstack/coord`. +- Implemented the gateway `MosCoordinationService`, deriving requester identity from trusted configuration and actor/tenant/correlation from authenticated context. +- Added contract and gateway boundary tests for configurable identities, native round-trip, unconfigured requester, self-delegation, target drift, and cross-tenant observe/result denial before adapter invocation. +- Did not modify `apps/gateway/src/commands/command-authorization.service.ts`. + +## Verification + +- `pnpm --filter @mosaicstack/coord test` — PASS (16 tests after authority/idempotency remediation). +- `pnpm --filter @mosaicstack/coord build` — PASS. +- `pnpm --filter @mosaicstack/gateway test -- mos-coordination.service.test.ts` — PASS (7 tests after authority/idempotency remediation). +- Standalone gateway typecheck initially reported missing built workspace packages after fresh worktree setup; root validation builds the workspace graph and passed. +- `TURBO_FORCE=true pnpm typecheck` — PASS (42 tasks, 0 cached). +- `TURBO_FORCE=true pnpm lint` — PASS (23 tasks, 0 cached after one import-type remediation). +- `TURBO_FORCE=true pnpm format:check` — PASS. +- `TURBO_FORCE=true pnpm test` — PASS (42 tasks, 0 cached; expected existing integration skips only). + +## Review checkpoint + +- Codex code review found idempotency keys needed actor scope and concurrent retries needed an in-flight reservation; both were remediated with regression coverage. +- Codex security review found whitespace-equivalent self-delegation was accepted by the exported client; identities are now normalized before invariant checks, with regression coverage. +- Re-review added immutable payload comparison for idempotency reuse, runtime string/size validation, bounded TTL/capacity tracking for gateway and native adapter state, and fresh-correlation follow-up reads; targeted tests pass (16 coord / 7 gateway). +- Final Codex security review found no issues. PR #735 was opened from commit `7936e15d`; Woodpecker pipeline #1752 is green. diff --git a/docs/scratchpads/tess-m4-003-operator-plugins.md b/docs/scratchpads/tess-m4-003-operator-plugins.md new file mode 100644 index 0000000..90c690d --- /dev/null +++ b/docs/scratchpads/tess-m4-003-operator-plugins.md @@ -0,0 +1,27 @@ +# TESS-M4-003 — Operator Plugin Foundations + +- **Task:** TESS-M4-003 / TESS-MEM-001 +- **Branch/base:** `feat/tess-operator-plugins` rebased on `origin/main` `76325ca3` +- **Scope:** first leaf-package memory/retrieval slice only; no durable inbox ownership, gateway integration, or Mosaic catalog implementation. + +## Handoff + +Coder4's uncommitted implementation was preserved first in commit `5b99c821` before review. The completion pass corrected the contract so namespace is injected configuration rather than caller-selected scope data, storage keys include tenant/owner/session via collision-safe tuple encoding, and malformed runtime scope values fail closed. + +## Delivered boundary + +- `OperatorMemoryPlugin` exposes `capture`, `search`, `recent`, `stats`, and `startupContext` through `MemoryAdapter` only. +- Scope is server-derived `{tenantId, ownerId, sessionId}`; adapter and namespace are configuration, not operation input. +- Capture redacts before persistence and records configured instance/namespace/source provenance. +- Wildcard retrieval is documented at the `MemoryAdapter` boundary and implemented by the keyword adapter. +- Startup context uses a bounded 64-result candidate window, then prioritizes project and flat-file provenance before slicing the configured output limit. +- No `Tess` identity is hardcoded in storage keys or defaults; tests use configured `Nova`. + +## Verification + +- `pnpm --filter @mosaicstack/memory test` — PASS (32 tests) +- `pnpm --filter @mosaicstack/memory typecheck` — PASS +- `pnpm --filter @mosaicstack/memory lint` — PASS +- `pnpm --filter @mosaicstack/memory build` — PASS +- Codex code review — APPROVE after remediation +- Codex security review — no findings after runtime scope-validation remediation diff --git a/docs/tess/ARCHITECTURE.md b/docs/tess/ARCHITECTURE.md index bd8e647..d268f52 100644 --- a/docs/tess/ARCHITECTURE.md +++ b/docs/tess/ARCHITECTURE.md @@ -41,7 +41,7 @@ Every call receives an immutable, server-derived actor/tenant/channel scope and `@mosaicstack/agent` owns the explicit `AgentRuntimeProviderRegistry`; duplicate provider IDs are rejected rather than replaced. Gateway owns `RuntimeProviderService`, which creates a frozen `RuntimeScope` from authenticated `ActorTenantScope` and trusted ingress channel/correlation metadata before every provider call. The service checks the declared provider capability before invoking a side effect and records metadata-only audit events (`providerId`, operation, outcome, actor/tenant/channel, correlation, and resource ID). It never records message bodies, idempotency keys, or approval references. -Termination is fail-closed: a runtime approval verifier must consume a one-time, exact action binding for the provider, session, actor, tenant, channel, and correlation ID before `terminate` reaches a provider. Until the durable verifier is wired, the default verifier denies termination. This internal service introduces no HTTP endpoint; later Discord, CLI, MCP, and provider adapters consume the same gateway boundary. +Termination is fail-closed: a runtime approval verifier consumes a one-time, exact action binding for the provider, session, actor, tenant, channel, and correlation ID before `terminate` reaches a provider. The verifier reuses the Redis-backed `interaction:command-approval:*` store and its expiry/delete-on-consume semantics; it has no parallel approval store. This internal service introduces no HTTP endpoint; later Discord, CLI, MCP, and provider adapters consume the same gateway boundary. ## Authority Model @@ -52,11 +52,49 @@ Termination is fail-closed: a runtime approval verifier must consume a one-time, | Destructive, privileged, external/customer-visible action | Human approval + policy | Propose, wait for durable one-time approval, then execute idempotently | | Provider-specific unsupported action | None | Fail closed; never emulate silently | +### Mos Coordination Boundary + +`@mosaicstack/coord` exposes only the transport-neutral `MosCoordinationPort` +verbs `handoff`, `observe`, and `result`. Gateway derives the actor, tenant, +correlation, and interaction-agent identity from authenticated context plus +trusted configuration; callers never provide an orchestration target. It +rejects unconfigured identities, self-delegation, target/correlation drift, and +cross-tenant handoff reads before an adapter call. No dispatch, assignment, +review, merge, or cancellation API exists at this boundary. + +M4 uses a deterministic native in-process queue adapter to prove the handoff → +observe → result flow without coupling the contract to tmux. A fleet/tmux +adapter is deferred to the M5 live-deployment seam and must implement the same +port. + ## Session and State Model A Tess session has stable `sessionId`, `tenantId`, `ownerId`, provider/runtime identity, ingress bindings, cursor, checkpoint, inbox/outbox, and idempotency records. Discord and CLI bind to the same authorized session. Ownership is verified server-side on every list/read/attach/send/terminate operation. -Valkey may hold ephemeral coordination state; PostgreSQL is canonical for durable session bindings, approvals, audit, checkpoints, inbox/outbox, and idempotency. Pi session files are replay sources, not cross-agent truth. +Valkey holds the existing short-lived, one-time command-approval records; PostgreSQL is canonical for durable session bindings, checkpoints, inbox/outbox, and idempotency. Pi session files are replay sources, not cross-agent truth. + +### M2 Durable Recovery + +`@mosaicstack/agent` owns a transport-neutral state machine and `apps/gateway` provides its +PostgreSQL adapter. `interaction_sessions` holds immutable identity; inbox/outbox records use a +per-session unique idempotency key and transition `pending → processing → processed|delivered`. +Checkpoints are immutable history scoped by session and checkpoint ID: the latest checkpoint +supports compaction recovery, while a handoff always resolves the exact checkpoint it references. +Recovery requeues only interrupted inbox work; an ambiguous `processing` outbox record is preserved +until separately authorized reconciliation can establish its external delivery state. + +Provider sends travel through the existing `RuntimeProviderService` with the persisted outbox +idempotency key. A normal dispatch claims exactly one outbox record and verifies its stored +correlation and channel against the server-derived request scope; it never requeues or drains +another live record. Inbox/outbox payloads and checkpoint cursor/summary pass through the existing +secret/PII redactor and AES-256-GCM sealing before persistence; decryption occurs only in the +scoped gateway repository path, and runtime audit remains metadata-only. + +An external effect cannot share a database transaction. If a process dies after an effect begins +but before its terminal outbox transition, automatic recovery does not replay that ambiguous claim. +It remains `processing` until separately authorized reconciliation can establish delivery state; +completed effects are never redispatched. Operators can therefore restart the gateway/Pi service, +reconstruct the session, and resume pending inbox work without relying on process-local state. ## Transport Strategy @@ -81,3 +119,5 @@ The provider advertises list, tree, read-only attach, send, and terminate. List/ ## Deployment Tess runs as a rostered, systemd-supervised Pi agent using GPT-5.6 Sol and high reasoning. Secrets are supplied through approved runtime secret mechanisms. Startup fails when required model, gateway identity, Discord binding, or durable-state dependencies are missing. Health reports effective model/reasoning/tool policy without credential material. + +The interaction-service identity is provisioning data, not a source identifier: the roster and per-agent environment carry the chosen display/roster name into a generic systemd instance. The service rejects a name mismatch or any drift from its pinned Pi/GPT-5.6 Sol/high/operator-interaction effective policy before launch. Its policy printer exposes only those resolved safe fields. diff --git a/docs/tess/M4-003-OPERATOR-PLUGIN-SKETCH.md b/docs/tess/M4-003-OPERATOR-PLUGIN-SKETCH.md new file mode 100644 index 0000000..5bacf6a --- /dev/null +++ b/docs/tess/M4-003-OPERATOR-PLUGIN-SKETCH.md @@ -0,0 +1,17 @@ +# TESS-M4-003 Operator Plugin Sketch + +## Memory/retrieval slice — TESS-MEM-001 + +Introduce a transport-neutral `OperatorMemoryPlugin` in `packages/memory`. The plugin receives a server-derived `{tenantId, ownerId, sessionId}` scope and delegates to a registered `MemoryAdapter`; adapter and namespace are injected configuration, never caller input. Its operations are `capture`, `search`, `recent`, `stats`, and `startupContext`. Results carry configured instance, provenance, and namespace metadata. Capture/redaction occurs before adapter persistence; startup context uses a bounded candidate window ordered so project/flat-file truth takes precedence within returned material. + +Registration remains replaceable-adapter based: the existing `registerMemoryAdapter(kind, factory)` / `createMemoryAdapter(config)` seam supplies the injected adapter to `createOperatorMemoryPlugin(config)`. Identity and namespace are configuration data; no interaction-agent name is embedded in keys or defaults. + +## Remaining plugin foundations — TESS-PLG-001 + +- `packages/agent`: capability descriptors for runtime bootstrap, durable inbox/state hooks, and read-only fleet diagnostics. Each capability advertises supported operations and fails closed when absent. +- `packages/mosaic`: a catalog/registration surface for GitOps, fleet diagnostics, runtime bootstrap, Discord, and MCP/skill discovery. Catalog entries describe authority, input schema, and safe/read-only status; they do not invoke provider transports directly. +- Gateway/channel adapters consume these contracts through server-derived actor/tenant context and durable session state, preserving the replaceable-adapter boundary. + +## First implementation boundary + +The first PR slice should add the operator-memory plugin contract, configuration-injected adapter seam, scope isolation, provenance-bearing retrieval, and tests for namespace isolation plus a differently named configured instance. Durable inbox/outbox remains owned by the existing `DurableSessionCoordinator`; this plugin only supplies bounded context/capture at lifecycle boundaries. diff --git a/docs/tess/MOS-COORDINATION.md b/docs/tess/MOS-COORDINATION.md new file mode 100644 index 0000000..2eb3c81 --- /dev/null +++ b/docs/tess/MOS-COORDINATION.md @@ -0,0 +1,83 @@ +# Tess–Mos Coordination Contract Sketch + +**Task:** TESS-M4-001 · **PRD:** TESS-MOS-001 / AC-TESS-04 + +## Boundary + +Agent identities are deployment data. A configured interaction agent may request +Mos-owned work; the configured orchestration agent owns decomposition, worker +assignment, reviews, and merge decisions. The interaction agent receives a +correlated receipt, read-only activity projection, and terminal result. It has +no dispatch, assignment, review, merge, or cancellation operation. + +## `@mosaicstack/coord` interface + +```ts +interface CoordinationScope { + readonly actorId: string; + readonly tenantId: string; + readonly correlationId: string; + readonly requesterAgentId: string; // trusted gateway/configuration data +} + +interface MosHandoffRequest { + readonly idempotencyKey: string; + readonly summary: string; + readonly context?: string; + readonly missionId?: string; +} + +interface MosHandoffReceipt { + readonly handoffId: string; + readonly targetAgentId: string; + readonly status: 'accepted' | 'queued'; + readonly correlationId: string; +} + +interface MosHandoff { + readonly handoffId: string; + readonly targetAgentId: string; + readonly request: MosHandoffRequest; + readonly scope: CoordinationScope; +} + +interface MosCoordinationPort { + handoff(handoff: MosHandoff): Promise; + observe(handoffId: string, scope: CoordinationScope): Promise; + result(handoffId: string, scope: CoordinationScope): Promise; +} +``` + +The port deliberately omits generic orchestrator verbs. It is tenant- and +correlation-scoped; its gateway implementation obtains `actorId`, `tenantId`, +and the requester agent from trusted authentication/configuration only. + +## Enforcement point + +`apps/gateway` owns a `MosCoordinationService` boundary that compares the +trusted configured requester/target identities and rejects all of the following +before calling a transport: unconfigured requester, self-delegation, target +identity drift, cross-tenant observe/result lookup, and attempts to observe or +receive a result for a handoff outside the originating tenant. The service exposes handoff, observe, +and result only, and delegates delivery to an injected adapter. + +M4 ships a native in-process `InMemoryMosCoordinationPort` as the concrete, +deterministic adapter. It preserves the immutable handoff ID, tenant, requester +identity, and correlation ID while demonstrating the handoff → observe → result +round trip. It is a queue/port adapter, not a Mos-side consumer. + +A future fleet/tmux adapter is a documented M5 deployment seam and must +implement the same `MosCoordinationPort`; no channel client or interaction +runtime calls a transport directly. + +## Required tests + +1. A configured non-default interaction identity can hand off work to a + configured non-default orchestration identity and receive its result. +2. The gateway passes only server-derived scope/identity to the adapter. +3. Self-targeting, target drift, and cross-tenant observe/result all fail closed + without invoking the adapter. +4. The exported public contract has no worker-dispatch, assignment, review, + merge, or cancellation capability. +5. The native adapter round-trips queued work, activity, and a host-recorded + terminal result without a live fleet dependency. diff --git a/docs/tess/VERIFICATION-MATRIX.md b/docs/tess/VERIFICATION-MATRIX.md index ab71dfd..d89b23c 100644 --- a/docs/tess/VERIFICATION-MATRIX.md +++ b/docs/tess/VERIFICATION-MATRIX.md @@ -5,7 +5,7 @@ | AC-TESS-01 | TESS-PI-001, TESS-DSC-001, TESS-CLI-001 | Discord/CLI same-session integration and streaming E2E | M3-V | | AC-TESS-02 | TESS-ARP-001, TESS-CLI-001, TESS-FLT-001 | CLI contract tests for status/sessions/tree/attach/send/stop, typed denial/error snapshots | M3-V | | AC-TESS-03 | TESS-PI-001, TESS-OBS-001 | Clean service launch; status asserts GPT-5.6 Sol, high reasoning and effective tool policy with secret canaries absent | M2-V, M3-V | -| AC-TESS-04 | TESS-MOS-001, TESS-FLT-001 | Authority E2E: coding request creates Mos handoff; safe status runs in Tess; no competing worker claim | M4-V | +| AC-TESS-04 | TESS-MOS-001, TESS-FLT-001 | M4 contract/gateway native-port handoff → observe → result round trip; configurable identity, target-drift and tenant-denial tests; M4-V fleet authority qualification | M4-001, M4-V | | AC-TESS-05 | TESS-HRM-001, TESS-MEM-001 | Hermes capability contract suite: sessions/stream/send/tree plus Kanban/skills/memory/tools/cron supported-or-denied matrix; operator-memory plugin (TESS-MEM-001) reachable end-to-end — env-configured plugin registered + AgentService session-bound server-derived {tenantId,ownerId,sessionId} scoped search/capture, cross-tenant reuse denied before plugin call (M4-W-001 spine: #736 plugin + #739 consumer) | M4-V | | AC-TESS-06 | TESS-STA-001, TESS-SEC-008 | Kill/restart/compaction fault injection across inbox/outbox/checkpoint transitions; duplicate side-effect detector | M2-V, M5-V | | AC-TESS-07 | TESS-SEC-001..009 | Threat-model abuse suite: authz, tenant isolation, forged identity/approval, injection, redaction, transport identity, GC scope | M1-V, M3-V, M5-V | diff --git a/docs/tess/hermes-runtime-adapter-design.md b/docs/tess/hermes-runtime-adapter-design.md new file mode 100644 index 0000000..4623c25 --- /dev/null +++ b/docs/tess/hermes-runtime-adapter-design.md @@ -0,0 +1,19 @@ +# TESS-HRM-001 — Hermes runtime adapter boundary + +## Normalized provider surface + +`HermesRuntimeProvider` implements the existing Mosaic-owned `AgentRuntimeProvider` unchanged. Its public surface is therefore `capabilities`, `health`, session list/tree, stream, send, attach/detach, and terminate, accepting only `RuntimeScope`, `RuntimeMessage`, `RuntimeSession`, `RuntimeStreamEvent`, and other types from `@mosaicstack/types`. Provider id is `runtime.hermes`. + +The provider receives a narrow injected `HermesRuntimeTransport`, whose method names and inputs may represent Hermes API operations but whose return values are explicitly private `HermesLegacy*` types defined only in `packages/agent/src/hermes-runtime-provider.ts`. Mapping functions convert those private values to Mosaic sessions, state, hierarchy, and stream events. Capability negotiation maps a supplied Hermes feature inventory onto the fixed Mosaic runtime capability vocabulary; no unknown/ambiguous legacy feature is advertised. Unsupported Mosaic operations throw the typed fail-closed `capability_unsupported` provider error before a transport call. + +## Boundary line + +**Hermes legacy schema ends at `HermesRuntimeTransport` and its private adapter-local `HermesLegacy*` definitions in `packages/agent`.** `packages/types` is never changed to contain a Hermes field, enum, identifier, session shape, status, or capability. `apps/gateway` registers/resolves the provider only through `AgentRuntimeProvider` and receives normalized values only. Identity remains server-derived `RuntimeScope` data and is passed to the injected transport as context, never reconstructed from a legacy response. + +## Initial mapping and safety posture + +- Hermes conversation/thread identifiers map to opaque Mosaic `RuntimeSession.id`; parent linkage maps only when a known parent exists. +- Hermes status strings map through a closed lookup to `RuntimeSessionState`; unknown statuses become `failed`, never a permissive active state. +- Legacy stream chunks map to `message.delta` / `message.complete`; malformed or unsupported events become a normalized `runtime.error` event. +- Send, attach, and terminate require the normalized capability first. `terminate` continues to be approval-bound by the gateway service; the adapter does not weaken gateway authority. +- Kanban, skills, memory, tools, and cron are capability-inventory entries for this transitional adapter, not additions to the core runtime contract. They are reported as explicitly unsupported until a Mosaic-owned capability contract exists. diff --git a/packages/agent/src/hermes-runtime-provider.test.ts b/packages/agent/src/hermes-runtime-provider.test.ts new file mode 100644 index 0000000..e5577ee --- /dev/null +++ b/packages/agent/src/hermes-runtime-provider.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { RuntimeScope } from '@mosaicstack/types'; +import { HermesRuntimeProvider, type HermesRuntimeTransport } from './hermes-runtime-provider.js'; + +const scope: RuntimeScope = { actorId: 'a', tenantId: 't', channelId: 'c', correlationId: 'r' }; +const transport = (capabilities = ['session.list', 'session.tree']): HermesRuntimeTransport => ({ + capabilities: vi.fn(async () => capabilities), + health: vi.fn(async () => ({ status: 'healthy' })), + sessions: vi.fn(async () => [ + { + conversation_id: 'child', + agent_id: 'hermes-a', + parent_conversation_id: 'parent', + status: 'running', + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + }, + { + conversation_id: 'parent', + agent_id: 'hermes-a', + status: 'unknown', + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + }, + ]), + stream: async function* () {}, + send: vi.fn(), + attach: vi.fn(), + detach: vi.fn(), + terminate: vi.fn(), +}); +describe('HermesRuntimeProvider normalization boundary', () => { + it('returns an exhaustive fail-closed transitional capability matrix', async () => { + const provider = new HermesRuntimeProvider(transport()); + + await expect(provider.transitionalCapabilityMatrix(scope)).resolves.toEqual([ + { capability: 'kanban', status: 'unsupported' }, + { capability: 'skills', status: 'unsupported' }, + { capability: 'memory', status: 'unsupported' }, + { capability: 'tools', status: 'unsupported' }, + { capability: 'cron', status: 'unsupported' }, + ]); + }); + + it('denies unsupported transitional capabilities without calling Hermes', async () => { + const hermes = transport(); + const provider = new HermesRuntimeProvider(hermes); + + await expect(provider.assertTransitionalCapability('memory', scope)).rejects.toMatchObject({ + code: 'capability_unsupported', + }); + expect(hermes.capabilities).not.toHaveBeenCalled(); + }); + + it('normalizes legacy sessions without exposing legacy fields', async () => { + const provider = new HermesRuntimeProvider(transport()); + await expect(provider.listSessions(scope)).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 'child', runtimeId: 'hermes-a', state: 'active' }), + ]), + ); + const result = await provider.listSessions(scope); + expect(result[0]).not.toHaveProperty('conversation_id'); + }); + it('forms normalized hierarchy and fails closed for unbridged operations', async () => { + const provider = new HermesRuntimeProvider(transport()); + await expect(provider.getSessionTree(scope)).resolves.toEqual([ + expect.objectContaining({ + session: expect.objectContaining({ id: 'parent', state: 'failed' }), + children: [expect.objectContaining({ session: expect.objectContaining({ id: 'child' }) })], + }), + ]); + await expect( + provider.sendMessage('parent', { content: 'x', idempotencyKey: 'i' }, scope), + ).rejects.toMatchObject({ code: 'capability_unsupported' }); + }); +}); diff --git a/packages/agent/src/hermes-runtime-provider.ts b/packages/agent/src/hermes-runtime-provider.ts new file mode 100644 index 0000000..8e4bb51 --- /dev/null +++ b/packages/agent/src/hermes-runtime-provider.ts @@ -0,0 +1,217 @@ +import type { + AgentRuntimeProvider, + RuntimeAttachHandle, + RuntimeAttachMode, + RuntimeCapability, + RuntimeCapabilitySet, + RuntimeHealth, + RuntimeMessage, + RuntimeScope, + RuntimeSession, + RuntimeSessionState, + RuntimeSessionTree, + RuntimeStreamEvent, + TransitionalCapabilityInventoryEntry, + TransitionalCapabilityInventoryProvider, + TransitionalRuntimeCapability, +} from '@mosaicstack/types'; + +const HERMES_PROVIDER_ID = 'runtime.hermes'; +const TRANSITIONAL_CAPABILITIES: readonly TransitionalRuntimeCapability[] = [ + 'kanban', + 'skills', + 'memory', + 'tools', + 'cron', +]; +const RUNTIME_CAPABILITIES: readonly RuntimeCapability[] = [ + 'session.list', + 'session.tree', + 'session.stream', + 'session.send', + 'session.attach', + 'session.terminate', +]; + +/** Legacy transport boundary. These shapes are intentionally adapter-local. */ +export interface HermesLegacySession { + conversation_id: string; + agent_id: string; + parent_conversation_id?: string; + status: string; + created_at: string; + updated_at: string; +} +export interface HermesRuntimeTransport { + capabilities(scope: RuntimeScope): Promise; + health(scope: RuntimeScope): Promise<{ status: string; detail?: string }>; + sessions(scope: RuntimeScope): Promise; + stream( + sessionId: string, + cursor: string | undefined, + scope: RuntimeScope, + ): AsyncIterable; + send(sessionId: string, message: RuntimeMessage, scope: RuntimeScope): Promise; + attach( + sessionId: string, + mode: RuntimeAttachMode, + scope: RuntimeScope, + ): Promise; + detach(attachmentId: string, scope: RuntimeScope): Promise; + terminate(sessionId: string, approvalRef: string, scope: RuntimeScope): Promise; +} + +export class HermesRuntimeProviderError extends Error { + constructor( + readonly code: 'capability_unsupported' | 'invalid_request', + message: string, + ) { + super(message); + this.name = HermesRuntimeProviderError.name; + } +} + +/** + * Transitional Hermes adapter. Legacy identifiers and schemas do not cross this + * boundary: callers only observe Mosaic AgentRuntimeProvider contracts. + */ +export class HermesRuntimeProvider + implements AgentRuntimeProvider, TransitionalCapabilityInventoryProvider +{ + readonly id = HERMES_PROVIDER_ID; + + constructor(private readonly transport: HermesRuntimeTransport) {} + + /** + * Full AC-TESS-05 migration inventory. These operations are deliberately + * unsupported until their Mosaic-owned plugin contracts exist. + */ + async transitionalCapabilityMatrix( + _scope: RuntimeScope, + ): Promise { + return TRANSITIONAL_CAPABILITIES.map((capability) => ({ capability, status: 'unsupported' })); + } + + async assertTransitionalCapability( + capability: TransitionalRuntimeCapability, + scope: RuntimeScope, + ): Promise { + const entry = (await this.transitionalCapabilityMatrix(scope)).find( + (candidate) => candidate.capability === capability, + ); + if (!entry || entry.status !== 'supported') { + throw new HermesRuntimeProviderError( + 'capability_unsupported', + `Hermes transitional capability is unsupported: ${capability}`, + ); + } + } + + async capabilities(scope: RuntimeScope): Promise { + const legacyCapabilities = await this.transport.capabilities(scope); + return { + supported: RUNTIME_CAPABILITIES.filter((capability) => + legacyCapabilities.includes(capability), + ), + }; + } + + async health(scope: RuntimeScope): Promise { + const health = await this.transport.health(scope); + return { + status: health.status === 'healthy' || health.status === 'degraded' ? health.status : 'down', + checkedAt: new Date().toISOString(), + ...(health.detail ? { detail: health.detail } : {}), + }; + } + + async listSessions(scope: RuntimeScope): Promise { + await this.requireCapability('session.list', scope); + return (await this.transport.sessions(scope)).map((session) => this.session(session)); + } + + async getSessionTree(scope: RuntimeScope): Promise { + await this.requireCapability('session.tree', scope); + const sessions = (await this.transport.sessions(scope)).map((session) => this.session(session)); + const nodes = new Map( + sessions.map((session) => [session.id, { session, children: [] }]), + ); + const roots: RuntimeSessionTree[] = []; + for (const session of sessions) { + const node = nodes.get(session.id)!; + const parent = session.parentSessionId ? nodes.get(session.parentSessionId) : undefined; + if (parent) parent.children.push(node); + else roots.push(node); + } + return roots; + } + + async *streamSession( + sessionId: string, + cursor: string | undefined, + scope: RuntimeScope, + ): AsyncIterable { + await this.requireCapability('session.stream', scope); + yield* this.transport.stream(sessionId, cursor, scope); + } + async sendMessage( + sessionId: string, + message: RuntimeMessage, + scope: RuntimeScope, + ): Promise { + await this.requireCapability('session.send', scope); + if (!message.content.trim()) + throw new HermesRuntimeProviderError('invalid_request', 'Message content is required'); + await this.transport.send(sessionId, message, scope); + } + async attach( + sessionId: string, + mode: RuntimeAttachMode, + scope: RuntimeScope, + ): Promise { + await this.requireCapability('session.attach', scope); + return this.transport.attach(sessionId, mode, scope); + } + async detach(attachmentId: string, scope: RuntimeScope): Promise { + await this.transport.detach(attachmentId, scope); + } + async terminate(sessionId: string, approvalRef: string, scope: RuntimeScope): Promise { + await this.requireCapability('session.terminate', scope); + if (!approvalRef.trim()) + throw new HermesRuntimeProviderError('invalid_request', 'Termination approval is required'); + await this.transport.terminate(sessionId, approvalRef, scope); + } + + private async requireCapability( + capability: RuntimeCapability, + scope: RuntimeScope, + ): Promise { + if (!(await this.capabilities(scope)).supported.includes(capability)) { + throw new HermesRuntimeProviderError( + 'capability_unsupported', + `Hermes does not bridge ${capability}`, + ); + } + } + private session(value: HermesLegacySession): RuntimeSession { + return { + id: value.conversation_id, + providerId: this.id, + runtimeId: value.agent_id, + ...(value.parent_conversation_id ? { parentSessionId: value.parent_conversation_id } : {}), + state: state(value.status), + createdAt: value.created_at, + updatedAt: value.updated_at, + }; + } +} +function state(value: string): RuntimeSessionState { + return ( + ( + { running: 'active', waiting: 'idle', starting: 'starting', stopped: 'stopped' } as Record< + string, + RuntimeSessionState + > + )[value] ?? 'failed' + ); +} diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index c4f2fc8..f7b1569 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -2,3 +2,5 @@ export const VERSION = '0.0.0'; export * from './runtime-provider-registry.js'; export * from './tmux-fleet-runtime-provider.js'; +export * from './hermes-runtime-provider.js'; +export * from './tess-durable-session.js'; diff --git a/packages/agent/src/tess-durable-session.test.ts b/packages/agent/src/tess-durable-session.test.ts new file mode 100644 index 0000000..3cadaf4 --- /dev/null +++ b/packages/agent/src/tess-durable-session.test.ts @@ -0,0 +1,305 @@ +import { describe, expect, it } from 'vitest'; +import { + DurableSessionCoordinator, + InMemoryDurableSessionStore, + type DurableSessionIdentity, +} from './tess-durable-session.js'; + +const IDENTITY: DurableSessionIdentity = { + agentName: 'Nova', + sessionId: 'tess-session-1', + tenantId: 'tenant-1', + ownerId: 'owner-1', + providerId: 'fleet', + runtimeSessionId: 'nova', +}; + +describe('DurableSessionCoordinator', () => { + it('reconstructs an exact session identity, pending inbox/outbox, checkpoint, and handoff after a simulated process restart', async () => { + const store = new InMemoryDurableSessionStore(); + const beforeRestart = new DurableSessionCoordinator(store); + + await beforeRestart.create(IDENTITY); + await beforeRestart.receive({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'ingress-1', + correlationId: 'correlation-1', + content: 'continue the session', + }); + await beforeRestart.enqueueOutbox({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'outbox-1', + correlationId: 'correlation-1', + channelId: 'cli', + kind: 'provider.send', + content: 'resumable response', + }); + await beforeRestart.checkpoint({ + sessionId: IDENTITY.sessionId, + checkpointId: 'checkpoint-1', + cursor: 'cursor-42', + summary: 'operator asked for recovery proof', + compactionEpoch: 0, + }); + await beforeRestart.handoff({ + sessionId: IDENTITY.sessionId, + handoffId: 'handoff-1', + destination: 'mos', + correlationId: 'correlation-1', + checkpointId: 'checkpoint-1', + status: 'pending', + }); + + // Simulate an ungraceful process death: no in-memory coordinator state survives. + const afterRestart = new DurableSessionCoordinator(store); + const recovered = await afterRestart.recover(IDENTITY.sessionId); + + expect(recovered.identity).toEqual(IDENTITY); + expect(recovered.inbox).toMatchObject([{ idempotencyKey: 'ingress-1', status: 'pending' }]); + expect(recovered.outbox).toMatchObject([{ idempotencyKey: 'outbox-1', status: 'pending' }]); + expect(recovered.checkpoint).toMatchObject({ + checkpointId: 'checkpoint-1', + cursor: 'cursor-42', + }); + expect(recovered.handoffs).toMatchObject([{ handoffId: 'handoff-1', status: 'pending' }]); + }); + + it('rebinds a recovered runtime while preserving the immutable conversation owner scope', async () => { + const coordinator = new DurableSessionCoordinator(new InMemoryDurableSessionStore()); + await coordinator.create(IDENTITY); + await coordinator.create({ + ...IDENTITY, + providerId: 'fleet-next', + runtimeSessionId: 'nova-next', + }); + + await expect(coordinator.snapshot(IDENTITY.sessionId)).resolves.toMatchObject({ + identity: { ...IDENTITY, providerId: 'fleet-next', runtimeSessionId: 'nova-next' }, + }); + await expect(coordinator.create({ ...IDENTITY, ownerId: 'other-owner' })).rejects.toThrow( + /identity conflict/, + ); + }); + + it('deduplicates duplicate ingress and never reprocesses an inbox record after restart or compaction', async () => { + const store = new InMemoryDurableSessionStore(); + const firstProcess = new DurableSessionCoordinator(store); + const handled: string[] = []; + + await firstProcess.create(IDENTITY); + await expect( + firstProcess.receive({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'ingress-duplicate', + correlationId: 'correlation-2', + content: 'only process me once', + }), + ).resolves.toMatchObject({ accepted: true }); + await expect( + firstProcess.receive({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'ingress-duplicate', + correlationId: 'correlation-2', + content: 'only process me once', + }), + ).resolves.toMatchObject({ accepted: false, status: 'pending' }); + await expect( + firstProcess.receive({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'ingress-duplicate', + correlationId: 'forged-correlation', + content: 'only process me once', + }), + ).rejects.toThrow(/idempotency conflict/); + + await firstProcess.drainInbox(IDENTITY.sessionId, async (entry) => { + handled.push(entry.idempotencyKey); + }); + await firstProcess.checkpoint({ + sessionId: IDENTITY.sessionId, + checkpointId: 'checkpoint-after-inbox', + cursor: 'cursor-43', + summary: 'safe to compact', + compactionEpoch: 1, + }); + + const afterRestartAndCompaction = new DurableSessionCoordinator(store); + await afterRestartAndCompaction.recover(IDENTITY.sessionId); + await afterRestartAndCompaction.drainInbox(IDENTITY.sessionId, async (entry) => { + handled.push(entry.idempotencyKey); + }); + + expect(handled).toEqual(['ingress-duplicate']); + }); + + it('does not redispatch an already applied outbox side effect after replay, restart, or compaction', async () => { + const store = new InMemoryDurableSessionStore(); + const beforeRestart = new DurableSessionCoordinator(store); + const appliedEffects: string[] = []; + + await beforeRestart.create(IDENTITY); + await beforeRestart.enqueueOutbox({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'effect-1', + correlationId: 'correlation-3', + channelId: 'cli', + kind: 'provider.send', + content: 'send exactly once', + }); + await expect( + beforeRestart.enqueueOutbox({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'effect-1', + correlationId: 'correlation-3', + channelId: 'cli', + kind: 'provider.send', + content: 'send exactly once', + }), + ).resolves.toMatchObject({ accepted: false, status: 'pending' }); + + await beforeRestart.dispatchOutbox(IDENTITY.sessionId, async (entry) => { + appliedEffects.push(entry.idempotencyKey); + }); + await beforeRestart.checkpoint({ + sessionId: IDENTITY.sessionId, + checkpointId: 'checkpoint-after-effect', + cursor: 'cursor-44', + summary: 'effect persisted before compaction', + compactionEpoch: 1, + }); + + const afterRestartAndCompaction = new DurableSessionCoordinator(store); + await afterRestartAndCompaction.recover(IDENTITY.sessionId); + await afterRestartAndCompaction.dispatchOutbox(IDENTITY.sessionId, async (entry) => { + appliedEffects.push(entry.idempotencyKey); + }); + + expect(appliedEffects).toEqual(['effect-1']); + }); + + it('rejects outbox idempotency-key reuse when immutable effect data differs', async () => { + const store = new InMemoryDurableSessionStore(); + const coordinator = new DurableSessionCoordinator(store); + + await coordinator.create(IDENTITY); + await coordinator.enqueueOutbox({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'outbox-conflict', + correlationId: 'correlation-outbox', + channelId: 'cli', + kind: 'provider.send', + content: 'original effect', + }); + await expect( + coordinator.enqueueOutbox({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'outbox-conflict', + correlationId: 'correlation-outbox', + channelId: 'forged-channel', + kind: 'provider.send', + content: 'original effect', + }), + ).rejects.toThrow(/idempotency conflict/); + }); + + it('retains an ambiguous failed provider effect as processing until explicit recovery', async () => { + const store = new InMemoryDurableSessionStore(); + const beforeRestart = new DurableSessionCoordinator(store); + + await beforeRestart.create(IDENTITY); + await beforeRestart.enqueueOutbox({ + sessionId: IDENTITY.sessionId, + idempotencyKey: 'ambiguous-effect', + correlationId: 'correlation-ambiguous', + channelId: 'cli', + kind: 'provider.send', + content: 'preserve this effect claim', + }); + await expect( + beforeRestart.dispatchOutbox(IDENTITY.sessionId, async (): Promise => { + throw new Error('provider connection dropped after submit'); + }), + ).rejects.toThrow(/connection dropped/); + + const afterRestart = new DurableSessionCoordinator(store); + await afterRestart.recover(IDENTITY.sessionId); + const calls: string[] = []; + await afterRestart.dispatchOutbox(IDENTITY.sessionId, async (entry): Promise => { + calls.push(entry.idempotencyKey); + }); + + expect(calls).toEqual([]); + expect(await afterRestart.snapshot(IDENTITY.sessionId)).toMatchObject({ + outbox: [{ idempotencyKey: 'ambiguous-effect', status: 'processing' }], + }); + }); + + it('rejects a handoff-id replay with different immutable state', async () => { + const store = new InMemoryDurableSessionStore(); + const coordinator = new DurableSessionCoordinator(store); + + await coordinator.create(IDENTITY); + await coordinator.checkpoint({ + sessionId: IDENTITY.sessionId, + checkpointId: 'checkpoint-conflict', + cursor: 'cursor-conflict', + summary: 'handoff conflict proof', + compactionEpoch: 0, + }); + await coordinator.handoff({ + sessionId: IDENTITY.sessionId, + handoffId: 'handoff-conflict', + destination: 'mos', + correlationId: 'correlation-conflict', + checkpointId: 'checkpoint-conflict', + status: 'pending', + }); + + await expect( + coordinator.handoff({ + sessionId: IDENTITY.sessionId, + handoffId: 'handoff-conflict', + destination: 'forged-destination', + correlationId: 'correlation-conflict', + checkpointId: 'checkpoint-conflict', + status: 'pending', + }), + ).rejects.toThrow(/identity conflict/); + }); + + it('keeps a handoff portable and resumes it from its durable checkpoint without process-local references', async () => { + const store = new InMemoryDurableSessionStore(); + const source = new DurableSessionCoordinator(store); + + await source.create(IDENTITY); + await source.checkpoint({ + sessionId: IDENTITY.sessionId, + checkpointId: 'checkpoint-handoff', + cursor: 'cursor-45', + summary: 'portable state', + compactionEpoch: 2, + }); + await source.handoff({ + sessionId: IDENTITY.sessionId, + handoffId: 'handoff-portable', + destination: 'mos', + correlationId: 'correlation-4', + checkpointId: 'checkpoint-handoff', + status: 'pending', + }); + await source.checkpoint({ + sessionId: IDENTITY.sessionId, + checkpointId: 'checkpoint-later', + cursor: 'cursor-46', + summary: 'newer compacted state must not strand the handoff', + compactionEpoch: 3, + }); + + const receivingProcess = new DurableSessionCoordinator(store); + const handoff = await receivingProcess.resumeHandoff('handoff-portable'); + + expect(handoff.identity).toEqual(IDENTITY); + expect(handoff.checkpoint).toMatchObject({ checkpointId: 'checkpoint-handoff' }); + expect(handoff.handoff).toMatchObject({ handoffId: 'handoff-portable', destination: 'mos' }); + }); +}); diff --git a/packages/agent/src/tess-durable-session.ts b/packages/agent/src/tess-durable-session.ts new file mode 100644 index 0000000..adc84c6 --- /dev/null +++ b/packages/agent/src/tess-durable-session.ts @@ -0,0 +1,498 @@ +export interface DurableSessionIdentity { + /** Provisioned roster identity; isolates durable state between named agents. */ + agentName: string; + sessionId: string; + tenantId: string; + ownerId: string; + providerId: string; + runtimeSessionId: string; +} + +export type DurableInboxStatus = 'pending' | 'processing' | 'processed'; +export type DurableOutboxStatus = 'pending' | 'processing' | 'delivered'; + +export interface DurableInboxInput { + sessionId: string; + idempotencyKey: string; + correlationId: string; + content: string; +} + +export interface DurableInboxEntry extends DurableInboxInput { + status: DurableInboxStatus; +} + +export interface DurableOutboxInput { + sessionId: string; + idempotencyKey: string; + correlationId: string; + channelId: string; + kind: string; + content: string; +} + +export interface DurableOutboxEntry extends DurableOutboxInput { + status: DurableOutboxStatus; +} + +export interface DurableCheckpointInput { + sessionId: string; + checkpointId: string; + cursor: string; + summary: string; + compactionEpoch: number; +} + +export interface DurableCheckpoint extends DurableCheckpointInput {} + +export interface DurableHandoffInput { + sessionId: string; + handoffId: string; + destination: string; + correlationId: string; + checkpointId: string; + status: 'pending' | 'accepted'; +} + +export interface DurableHandoff extends DurableHandoffInput {} + +export interface DurableSessionSnapshot { + identity: DurableSessionIdentity; + inbox: DurableInboxEntry[]; + outbox: DurableOutboxEntry[]; + checkpoint?: DurableCheckpoint; + handoffs: DurableHandoff[]; +} + +export interface DurableHandoffRecovery { + identity: DurableSessionIdentity; + checkpoint: DurableCheckpoint; + handoff: DurableHandoff; +} + +export interface DurableEnqueueResult { + accepted: boolean; + status: TStatus; +} + +/** + * A durable-state port. Implementations must atomically claim and complete work + * records. Recovery may requeue interrupted inbox work, but never an + * externally visible outbox effect: an ambiguous provider result remains + * claimed until a separately authorized reconciliation proves it safe. + */ +export interface DurableSessionStore { + create(identity: DurableSessionIdentity): Promise; + snapshot(sessionId: string): Promise; + enqueueInbox(input: DurableInboxInput): Promise>; + claimInbox(sessionId: string): Promise; + completeInbox(sessionId: string, idempotencyKey: string): Promise; + releaseInbox(sessionId: string, idempotencyKey: string): Promise; + enqueueOutbox(input: DurableOutboxInput): Promise>; + claimOutbox(sessionId: string): Promise; + claimOutboxByKey(sessionId: string, idempotencyKey: string): Promise; + completeOutbox(sessionId: string, idempotencyKey: string): Promise; + releaseOutbox(sessionId: string, idempotencyKey: string): Promise; + checkpoint(input: DurableCheckpointInput): Promise; + findCheckpoint(sessionId: string, checkpointId: string): Promise; + handoff(input: DurableHandoffInput): Promise; + findHandoff(handoffId: string): Promise; + requeueInFlight(sessionId: string): Promise; +} + +export class DurableSessionNotFoundError extends Error { + constructor(sessionId: string) { + super(`Durable Tess session not found: ${sessionId}`); + this.name = 'DurableSessionNotFoundError'; + } +} + +export class DurableSessionCoordinator { + constructor(private readonly store: DurableSessionStore) {} + + async create(identity: DurableSessionIdentity): Promise { + this.assertIdentity(identity); + await this.store.create(identity); + } + + async receive(input: DurableInboxInput): Promise> { + this.assertRecord(input.sessionId, input.idempotencyKey, input.correlationId, input.content); + return this.store.enqueueInbox(input); + } + + async enqueueOutbox( + input: DurableOutboxInput, + ): Promise> { + this.assertRecord( + input.sessionId, + input.idempotencyKey, + input.correlationId, + input.channelId, + input.content, + ); + if (input.kind.trim().length === 0) throw new Error('Durable outbox kind is required'); + return this.store.enqueueOutbox(input); + } + + async checkpoint(input: DurableCheckpointInput): Promise { + this.assertRecord(input.sessionId, input.checkpointId, input.cursor, input.summary); + if (!Number.isInteger(input.compactionEpoch) || input.compactionEpoch < 0) { + throw new Error('Durable checkpoint compaction epoch must be a non-negative integer'); + } + await this.store.checkpoint(input); + } + + async handoff(input: DurableHandoffInput): Promise { + this.assertRecord(input.sessionId, input.handoffId, input.destination, input.correlationId); + if (input.checkpointId.trim().length === 0) + throw new Error('Durable handoff checkpoint is required'); + await this.store.handoff(input); + } + + /** Read durable state without changing claim status; safe during normal operation. */ + async snapshot(sessionId: string): Promise { + const snapshot = await this.store.snapshot(sessionId); + if (!snapshot) throw new DurableSessionNotFoundError(sessionId); + return snapshot; + } + + /** Requeue interrupted inbox work during recovery; preserve ambiguous outbox claims. */ + async recover(sessionId: string): Promise { + await this.store.requeueInFlight(sessionId); + return this.snapshot(sessionId); + } + + async drainInbox( + sessionId: string, + handler: (entry: DurableInboxEntry) => Promise, + ): Promise { + for (;;) { + const entry = await this.store.claimInbox(sessionId); + if (!entry) return; + try { + await handler(entry); + } catch (error: unknown) { + await this.store.releaseInbox(sessionId, entry.idempotencyKey); + throw error; + } + // If this write fails after the handler succeeded, leave the record + // processing. A recovery path can retry it with its stable idempotency key. + await this.store.completeInbox(sessionId, entry.idempotencyKey); + } + } + + async dispatchOutbox( + sessionId: string, + dispatcher: (entry: DurableOutboxEntry) => Promise, + ): Promise { + for (;;) { + const entry = await this.store.claimOutbox(sessionId); + if (!entry) return; + // A provider failure can be ambiguous: it may occur after the receiver + // accepted the idempotency key. Preserve the claim for reconciliation. + await dispatcher(entry); + // Do not requeue an effect after it has been applied but before its + // terminal state could be persisted; recovery preserves the claim. + await this.store.completeOutbox(sessionId, entry.idempotencyKey); + } + } + + async dispatchOutboxEntry( + sessionId: string, + idempotencyKey: string, + dispatcher: (entry: DurableOutboxEntry) => Promise, + ): Promise { + const entry = await this.store.claimOutboxByKey(sessionId, idempotencyKey); + if (!entry) return; + // A provider failure can be ambiguous, so this stays processing until + // separately authorized reconciliation proves it safe to resolve. + await dispatcher(entry); + await this.store.completeOutbox(sessionId, entry.idempotencyKey); + } + + async resumeHandoff(handoffId: string): Promise { + const handoff = await this.store.findHandoff(handoffId); + if (!handoff) throw new Error(`Durable Tess handoff not found: ${handoffId}`); + const snapshot = await this.snapshot(handoff.sessionId); + const checkpoint = await this.store.findCheckpoint(handoff.sessionId, handoff.checkpointId); + if (!checkpoint) { + throw new Error(`Durable Tess handoff checkpoint is unavailable: ${handoff.checkpointId}`); + } + return { identity: snapshot.identity, checkpoint, handoff }; + } + + private assertIdentity(identity: DurableSessionIdentity): void { + this.assertRecord( + identity.agentName, + identity.sessionId, + identity.tenantId, + identity.ownerId, + identity.providerId, + ); + if (identity.runtimeSessionId.trim().length === 0) { + throw new Error('Durable runtime session identity is required'); + } + } + + private assertRecord(...values: string[]): void { + if (values.some((value: string): boolean => value.trim().length === 0)) { + throw new Error('Durable session records require non-empty fields'); + } + } +} + +interface InMemorySessionState { + identity: DurableSessionIdentity; + inbox: Map; + outbox: Map; + checkpoints: Map; + handoffs: Map; +} + +/** Reference store for deterministic domain tests; production uses the gateway DB adapter. */ +export class InMemoryDurableSessionStore implements DurableSessionStore { + private readonly sessions = new Map(); + + async create(identity: DurableSessionIdentity): Promise { + const existing = this.sessions.get(identity.sessionId); + if (existing) { + if (!sameEnrollmentScope(existing.identity, identity)) { + throw new Error(`Durable Tess session identity conflict: ${identity.sessionId}`); + } + existing.identity.providerId = identity.providerId; + existing.identity.runtimeSessionId = identity.runtimeSessionId; + return; + } + this.sessions.set(identity.sessionId, { + identity: copyIdentity(identity), + inbox: new Map(), + outbox: new Map(), + checkpoints: new Map(), + handoffs: new Map(), + }); + } + + async snapshot(sessionId: string): Promise { + const state = this.sessions.get(sessionId); + if (!state) return null; + const checkpoint = latestCheckpoint(state.checkpoints); + return { + identity: copyIdentity(state.identity), + inbox: [...state.inbox.values()].map(copyInbox), + outbox: [...state.outbox.values()].map(copyOutbox), + ...(checkpoint ? { checkpoint: copyCheckpoint(checkpoint) } : {}), + handoffs: [...state.handoffs.values()].map(copyHandoff), + }; + } + + async enqueueInbox(input: DurableInboxInput): Promise> { + const state = this.require(input.sessionId); + const existing = state.inbox.get(input.idempotencyKey); + if (existing) { + if (!sameInbox(existing, input)) { + throw new Error(`Durable Tess inbox idempotency conflict: ${input.idempotencyKey}`); + } + return { accepted: false, status: existing.status }; + } + state.inbox.set(input.idempotencyKey, { ...input, status: 'pending' }); + return { accepted: true, status: 'pending' }; + } + + async claimInbox(sessionId: string): Promise { + const state = this.require(sessionId); + const entry = [...state.inbox.values()].find( + (candidate: DurableInboxEntry): boolean => candidate.status === 'pending', + ); + if (!entry) return null; + entry.status = 'processing'; + return copyInbox(entry); + } + + async completeInbox(sessionId: string, idempotencyKey: string): Promise { + this.requireEntry(this.require(sessionId).inbox, idempotencyKey, 'inbox').status = 'processed'; + } + + async releaseInbox(sessionId: string, idempotencyKey: string): Promise { + this.requireEntry(this.require(sessionId).inbox, idempotencyKey, 'inbox').status = 'pending'; + } + + async enqueueOutbox( + input: DurableOutboxInput, + ): Promise> { + const state = this.require(input.sessionId); + const existing = state.outbox.get(input.idempotencyKey); + if (existing) { + if (!sameOutbox(existing, input)) { + throw new Error(`Durable Tess outbox idempotency conflict: ${input.idempotencyKey}`); + } + return { accepted: false, status: existing.status }; + } + state.outbox.set(input.idempotencyKey, { ...input, status: 'pending' }); + return { accepted: true, status: 'pending' }; + } + + async claimOutbox(sessionId: string): Promise { + const state = this.require(sessionId); + const entry = [...state.outbox.values()].find( + (candidate: DurableOutboxEntry): boolean => candidate.status === 'pending', + ); + if (!entry) return null; + entry.status = 'processing'; + return copyOutbox(entry); + } + + async claimOutboxByKey( + sessionId: string, + idempotencyKey: string, + ): Promise { + const entry = this.require(sessionId).outbox.get(idempotencyKey); + if (!entry || entry.status !== 'pending') return null; + entry.status = 'processing'; + return copyOutbox(entry); + } + + async completeOutbox(sessionId: string, idempotencyKey: string): Promise { + this.requireEntry(this.require(sessionId).outbox, idempotencyKey, 'outbox').status = + 'delivered'; + } + + async releaseOutbox(sessionId: string, idempotencyKey: string): Promise { + this.requireEntry(this.require(sessionId).outbox, idempotencyKey, 'outbox').status = 'pending'; + } + + async checkpoint(input: DurableCheckpointInput): Promise { + const checkpoints = this.require(input.sessionId).checkpoints; + const existing = checkpoints.get(input.checkpointId); + if (existing && !sameCheckpoint(existing, input)) { + throw new Error(`Durable Tess checkpoint identity conflict: ${input.checkpointId}`); + } + if (!existing) checkpoints.set(input.checkpointId, { ...input }); + } + + async findCheckpoint(sessionId: string, checkpointId: string): Promise { + const checkpoint = this.require(sessionId).checkpoints.get(checkpointId); + return checkpoint ? copyCheckpoint(checkpoint) : null; + } + + async handoff(input: DurableHandoffInput): Promise { + const state = this.require(input.sessionId); + if (!state.checkpoints.has(input.checkpointId)) { + throw new Error(`Durable Tess handoff checkpoint is unavailable: ${input.checkpointId}`); + } + const existing = state.handoffs.get(input.handoffId); + if (existing && !sameHandoff(existing, input)) { + throw new Error(`Durable Tess handoff identity conflict: ${input.handoffId}`); + } + if (!existing) state.handoffs.set(input.handoffId, { ...input }); + } + + async findHandoff(handoffId: string): Promise { + for (const state of this.sessions.values()) { + const handoff = state.handoffs.get(handoffId); + if (handoff) return copyHandoff(handoff); + } + return null; + } + + async requeueInFlight(sessionId: string): Promise { + const state = this.require(sessionId); + for (const entry of state.inbox.values()) { + if (entry.status === 'processing') entry.status = 'pending'; + } + } + + private require(sessionId: string): InMemorySessionState { + const state = this.sessions.get(sessionId); + if (!state) throw new DurableSessionNotFoundError(sessionId); + return state; + } + + private requireEntry( + records: Map, + idempotencyKey: string, + kind: string, + ): T { + const entry = records.get(idempotencyKey); + if (!entry) throw new Error(`Durable Tess ${kind} entry not found: ${idempotencyKey}`); + return entry; + } +} + +function sameEnrollmentScope(left: DurableSessionIdentity, right: DurableSessionIdentity): boolean { + return ( + left.agentName === right.agentName && + left.sessionId === right.sessionId && + left.tenantId === right.tenantId && + left.ownerId === right.ownerId + ); +} + +function sameInbox(left: DurableInboxEntry, right: DurableInboxInput): boolean { + return ( + left.sessionId === right.sessionId && + left.idempotencyKey === right.idempotencyKey && + left.correlationId === right.correlationId && + left.content === right.content + ); +} + +function sameOutbox(left: DurableOutboxEntry, right: DurableOutboxInput): boolean { + return ( + left.sessionId === right.sessionId && + left.idempotencyKey === right.idempotencyKey && + left.correlationId === right.correlationId && + left.channelId === right.channelId && + left.kind === right.kind && + left.content === right.content + ); +} + +function sameCheckpoint(left: DurableCheckpoint, right: DurableCheckpointInput): boolean { + return ( + left.sessionId === right.sessionId && + left.checkpointId === right.checkpointId && + left.cursor === right.cursor && + left.summary === right.summary && + left.compactionEpoch === right.compactionEpoch + ); +} + +function latestCheckpoint( + checkpoints: Map, +): DurableCheckpoint | undefined { + return [...checkpoints.values()].sort( + (left: DurableCheckpoint, right: DurableCheckpoint): number => + right.compactionEpoch - left.compactionEpoch, + )[0]; +} + +function sameHandoff(left: DurableHandoff, right: DurableHandoffInput): boolean { + return ( + left.sessionId === right.sessionId && + left.handoffId === right.handoffId && + left.destination === right.destination && + left.correlationId === right.correlationId && + left.checkpointId === right.checkpointId && + left.status === right.status + ); +} + +function copyIdentity(identity: DurableSessionIdentity): DurableSessionIdentity { + return { ...identity }; +} + +function copyInbox(entry: DurableInboxEntry): DurableInboxEntry { + return { ...entry }; +} + +function copyOutbox(entry: DurableOutboxEntry): DurableOutboxEntry { + return { ...entry }; +} + +function copyCheckpoint(checkpoint: DurableCheckpoint): DurableCheckpoint { + return { ...checkpoint }; +} + +function copyHandoff(handoff: DurableHandoff): DurableHandoff { + return { ...handoff }; +} diff --git a/packages/coord/src/__tests__/mos-coordination.test.ts b/packages/coord/src/__tests__/mos-coordination.test.ts new file mode 100644 index 0000000..912bdc6 --- /dev/null +++ b/packages/coord/src/__tests__/mos-coordination.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + InMemoryMosCoordinationPort, + MosCoordinationClient, + type CoordinationScope, + type MosCoordinationAuthorityError, + type MosCoordinationPort, +} from '../index.js'; + +const scope: CoordinationScope = { + actorId: 'operator-1', + tenantId: 'tenant-a', + correlationId: 'corr-1', + requesterAgentId: 'Nova', +}; + +function client( + port: MosCoordinationPort, + handoffIdFactory: () => string = (): string => 'handoff-1', +): MosCoordinationClient { + return new MosCoordinationClient( + { interactionAgentId: 'Nova', orchestrationAgentId: 'Conductor' }, + port, + handoffIdFactory, + ); +} + +describe('MosCoordinationClient', (): void => { + it('round-trips handoff, observation, and result through the native port with identities as data', async (): Promise => { + const adapter = new InMemoryMosCoordinationPort(); + const coordination = client(adapter); + + await expect( + coordination.handoff( + { idempotencyKey: 'handoff-request-1', summary: 'Implement the requested feature' }, + scope, + ), + ).resolves.toEqual({ + handoffId: 'handoff-1', + targetAgentId: 'Conductor', + status: 'queued', + correlationId: 'corr-1', + }); + + adapter.recordActivity('handoff-1', 'running', 'Mos accepted the request'); + adapter.recordResult('handoff-1', 'completed', 'Merged by Mos'); + + await expect(coordination.observe('handoff-1', scope)).resolves.toMatchObject({ + status: 'completed', + targetAgentId: 'Conductor', + activity: expect.arrayContaining([ + expect.objectContaining({ status: 'queued' }), + expect.objectContaining({ status: 'running' }), + expect.objectContaining({ status: 'completed' }), + ]), + }); + await expect(coordination.result('handoff-1', scope)).resolves.toEqual({ + handoffId: 'handoff-1', + targetAgentId: 'Conductor', + status: 'completed', + correlationId: 'corr-1', + summary: 'Merged by Mos', + }); + + expect(coordination).not.toHaveProperty('dispatch'); + expect(coordination).not.toHaveProperty('assign'); + expect(coordination).not.toHaveProperty('review'); + expect(coordination).not.toHaveProperty('merge'); + expect(coordination).not.toHaveProperty('cancel'); + }); + + it('fails closed before delivery when an unconfigured agent requests Mos work', async (): Promise => { + const adapter = new InMemoryMosCoordinationPort(); + + await expect( + client(adapter).handoff( + { idempotencyKey: 'handoff-request-1', summary: 'Implement the requested feature' }, + { ...scope, requesterAgentId: 'Untrusted' }, + ), + ).rejects.toMatchObject({ + code: 'requester_forbidden', + } satisfies Partial); + }); + + it('rejects self-delegation configuration before constructing a client', (): void => { + expect( + (): MosCoordinationClient => + new MosCoordinationClient( + { interactionAgentId: 'Nova', orchestrationAgentId: 'Nova' }, + new InMemoryMosCoordinationPort(), + ), + ).toThrow('Interaction and orchestration identities must differ'); + }); + + it('rejects whitespace-equivalent self-delegation identities', (): void => { + expect( + (): MosCoordinationClient => + new MosCoordinationClient( + { interactionAgentId: 'Nova ', orchestrationAgentId: 'Nova' }, + new InMemoryMosCoordinationPort(), + ), + ).toThrow('Interaction and orchestration identities must differ'); + }); + + it('does not expose another tenant handoff to observe or result', async (): Promise => { + const adapter = new InMemoryMosCoordinationPort(); + const coordination = client(adapter); + await coordination.handoff( + { idempotencyKey: 'handoff-request-1', summary: 'Implement the requested feature' }, + scope, + ); + + const otherTenantScope = { ...scope, tenantId: 'tenant-b' }; + await expect(coordination.observe('handoff-1', otherTenantScope)).rejects.toMatchObject({ + code: 'forbidden', + }); + await expect(coordination.result('handoff-1', otherTenantScope)).rejects.toMatchObject({ + code: 'forbidden', + }); + }); + + it('bounds native handoff retention by evicting the oldest handoff', async (): Promise => { + const adapter = new InMemoryMosCoordinationPort({ maxHandoffs: 1 }); + const first = client(adapter, (): string => 'handoff-1'); + const second = client(adapter, (): string => 'handoff-2'); + await first.handoff({ idempotencyKey: 'handoff-request-1', summary: 'First request' }, scope); + await second.handoff({ idempotencyKey: 'handoff-request-2', summary: 'Second request' }, scope); + + await expect(first.observe('handoff-1', scope)).rejects.toMatchObject({ code: 'not_found' }); + await expect(second.observe('handoff-2', scope)).resolves.toMatchObject({ status: 'queued' }); + }); + + it('fails closed when a transport reports target drift', async (): Promise => { + const adapter: MosCoordinationPort = { + handoff: vi.fn(async () => ({ + handoffId: 'handoff-1', + targetAgentId: 'Unexpected', + status: 'accepted' as const, + correlationId: 'corr-1', + })), + observe: vi.fn(), + result: vi.fn(), + }; + + await expect( + client(adapter).handoff( + { idempotencyKey: 'handoff-request-1', summary: 'Implement the requested feature' }, + scope, + ), + ).rejects.toMatchObject({ + code: 'target_drift', + } satisfies Partial); + }); +}); diff --git a/packages/coord/src/in-memory-mos-coordination-port.ts b/packages/coord/src/in-memory-mos-coordination-port.ts new file mode 100644 index 0000000..ddebbad --- /dev/null +++ b/packages/coord/src/in-memory-mos-coordination-port.ts @@ -0,0 +1,208 @@ +import { + type CoordinationObservation, + type CoordinationResult, + type CoordinationScope, + type MosCoordinationActivity, + type MosCoordinationPort, + type MosHandoff, + type MosHandoffReceipt, + type MosHandoffStatus, +} from './mos-coordination.js'; + +const DEFAULT_HANDOFF_TTL_MS = 60 * 60 * 1_000; +const DEFAULT_MAX_HANDOFFS = 1_000; + +interface StoredHandoff { + readonly handoff: MosHandoff; + status: MosHandoffStatus; + readonly activity: MosCoordinationActivity[]; + readonly expiresAt: number; + result?: CoordinationResult; +} + +export interface InMemoryMosCoordinationPortOptions { + now?: () => Date; + handoffTtlMs?: number; + maxHandoffs?: number; +} + +/** + * Native deterministic queue/port adapter for the coordination boundary. + * It intentionally has no fleet/tmux dependency. A future deployment adapter + * implements MosCoordinationPort without changing interaction-plane callers. + */ +export class InMemoryMosCoordinationPort implements MosCoordinationPort { + private readonly handoffs = new Map(); + private readonly now: () => Date; + private readonly handoffTtlMs: number; + private readonly maxHandoffs: number; + + constructor(options: InMemoryMosCoordinationPortOptions = {}) { + this.now = options.now ?? (() => new Date()); + this.handoffTtlMs = options.handoffTtlMs ?? DEFAULT_HANDOFF_TTL_MS; + this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS; + } + + async handoff(handoff: MosHandoff): Promise { + this.pruneExpiredHandoffs(); + const existing = this.handoffs.get(handoff.handoffId); + if (existing !== undefined) { + this.assertSameHandoff(existing.handoff, handoff); + return this.receipt(existing.handoff, existing.status); + } + + const stored: StoredHandoff = { + handoff: snapshotHandoff(handoff), + status: 'queued', + activity: [activity('queued', 'Handoff accepted by the native coordination queue', this.now)], + expiresAt: this.now().getTime() + this.handoffTtlMs, + }; + this.handoffs.set(handoff.handoffId, stored); + this.enforceHandoffLimit(); + return this.receipt(stored.handoff, stored.status); + } + + async observe(handoffId: string, scope: CoordinationScope): Promise { + this.pruneExpiredHandoffs(); + const stored = this.requireScopedHandoff(handoffId, scope); + return { + handoffId: stored.handoff.handoffId, + targetAgentId: stored.handoff.targetAgentId, + status: stored.status, + correlationId: stored.handoff.scope.correlationId, + activity: stored.activity.map(copyActivity), + }; + } + + async result(handoffId: string, scope: CoordinationScope): Promise { + this.pruneExpiredHandoffs(); + const stored = this.requireScopedHandoff(handoffId, scope); + return ( + stored.result ?? { + handoffId: stored.handoff.handoffId, + targetAgentId: stored.handoff.targetAgentId, + status: 'pending', + correlationId: stored.handoff.scope.correlationId, + } + ); + } + + /** Host-side progression seam; interaction clients never receive this capability. */ + recordActivity(handoffId: string, status: MosHandoffStatus, summary: string): void { + this.pruneExpiredHandoffs(); + const stored = this.requireHandoff(handoffId); + stored.status = status; + stored.activity.push(activity(status, summary, this.now)); + } + + /** Host-side result seam for deterministic qualification; not a Mos consumer. */ + recordResult(handoffId: string, status: 'completed' | 'failed', summary: string): void { + this.pruneExpiredHandoffs(); + const stored = this.requireHandoff(handoffId); + stored.status = status; + stored.activity.push(activity(status, summary, this.now)); + stored.result = { + handoffId: stored.handoff.handoffId, + targetAgentId: stored.handoff.targetAgentId, + status, + correlationId: stored.handoff.scope.correlationId, + summary, + }; + } + + private receipt(handoff: MosHandoff, status: MosHandoffStatus): MosHandoffReceipt { + return { + handoffId: handoff.handoffId, + targetAgentId: handoff.targetAgentId, + status: status === 'accepted' ? 'accepted' : 'queued', + correlationId: handoff.scope.correlationId, + }; + } + + private pruneExpiredHandoffs(): void { + const nowMs = this.now().getTime(); + for (const [handoffId, handoff] of this.handoffs) { + if (handoff.expiresAt <= nowMs) this.handoffs.delete(handoffId); + } + } + + private enforceHandoffLimit(): void { + while (this.handoffs.size > this.maxHandoffs) { + const oldest = this.handoffs.keys().next().value; + if (typeof oldest !== 'string') return; + this.handoffs.delete(oldest); + } + } + + private requireScopedHandoff(handoffId: string, scope: CoordinationScope): StoredHandoff { + const stored = this.requireHandoff(handoffId); + if ( + stored.handoff.scope.tenantId !== scope.tenantId || + stored.handoff.scope.actorId !== scope.actorId || + stored.handoff.scope.requesterAgentId !== scope.requesterAgentId + ) { + throw new InMemoryMosCoordinationError('forbidden', 'Handoff scope does not match'); + } + return stored; + } + + private requireHandoff(handoffId: string): StoredHandoff { + const stored = this.handoffs.get(handoffId); + if (stored === undefined) { + throw new InMemoryMosCoordinationError('not_found', 'Handoff was not found'); + } + return stored; + } + + private assertSameHandoff(existing: MosHandoff, incoming: MosHandoff): void { + if ( + existing.targetAgentId !== incoming.targetAgentId || + existing.request.idempotencyKey !== incoming.request.idempotencyKey || + existing.request.summary !== incoming.request.summary || + existing.request.context !== incoming.request.context || + existing.request.missionId !== incoming.request.missionId || + existing.scope.actorId !== incoming.scope.actorId || + existing.scope.tenantId !== incoming.scope.tenantId || + existing.scope.correlationId !== incoming.scope.correlationId || + existing.scope.requesterAgentId !== incoming.scope.requesterAgentId + ) { + throw new InMemoryMosCoordinationError( + 'conflict', + 'Handoff ID is already bound to different immutable input', + ); + } + } +} + +export type InMemoryMosCoordinationErrorCode = 'conflict' | 'forbidden' | 'not_found'; + +export class InMemoryMosCoordinationError extends Error { + constructor( + readonly code: InMemoryMosCoordinationErrorCode, + message: string, + ) { + super(message); + this.name = InMemoryMosCoordinationError.name; + } +} + +function activity( + status: MosHandoffStatus, + summary: string, + now: () => Date, +): MosCoordinationActivity { + return { occurredAt: now().toISOString(), status, summary }; +} + +function copyActivity(entry: MosCoordinationActivity): MosCoordinationActivity { + return { ...entry }; +} + +function snapshotHandoff(handoff: MosHandoff): MosHandoff { + return Object.freeze({ + handoffId: handoff.handoffId, + targetAgentId: handoff.targetAgentId, + request: Object.freeze({ ...handoff.request }), + scope: Object.freeze({ ...handoff.scope }), + }); +} diff --git a/packages/coord/src/index.ts b/packages/coord/src/index.ts index 7ac5309..cf59bac 100644 --- a/packages/coord/src/index.ts +++ b/packages/coord/src/index.ts @@ -2,6 +2,23 @@ export { createMission, loadMission, missionFilePath, saveMission } from './miss export { parseTasksFile, updateTaskStatus, writeTasksFile } from './tasks-file.js'; export { runTask, resumeTask } from './runner.js'; export { getMissionStatus, getTaskStatus } from './status.js'; +export { + InMemoryMosCoordinationError, + InMemoryMosCoordinationPort, +} from './in-memory-mos-coordination-port.js'; +export { MosCoordinationAuthorityError, MosCoordinationClient } from './mos-coordination.js'; +export type { + CoordinationObservation, + CoordinationResult, + CoordinationScope, + MosCoordinationActivity, + MosCoordinationIdentity, + MosCoordinationPort, + MosHandoff, + MosHandoffReceipt, + MosHandoffRequest, + MosHandoffStatus, +} from './mos-coordination.js'; export type { CreateMissionOptions, Mission, diff --git a/packages/coord/src/mos-coordination.ts b/packages/coord/src/mos-coordination.ts new file mode 100644 index 0000000..f71d2ea --- /dev/null +++ b/packages/coord/src/mos-coordination.ts @@ -0,0 +1,207 @@ +export type MosHandoffStatus = 'queued' | 'accepted' | 'running' | 'completed' | 'failed'; + +export interface CoordinationScope { + readonly actorId: string; + readonly tenantId: string; + readonly correlationId: string; + /** Trusted gateway/configuration identity; never supplied by a channel client. */ + readonly requesterAgentId: string; +} + +export interface MosCoordinationIdentity { + readonly interactionAgentId: string; + readonly orchestrationAgentId: string; +} + +export interface MosHandoffRequest { + readonly idempotencyKey: string; + readonly summary: string; + readonly context?: string; + readonly missionId?: string; +} + +export interface MosHandoff { + readonly handoffId: string; + readonly targetAgentId: string; + readonly request: MosHandoffRequest; + readonly scope: CoordinationScope; +} + +export interface MosHandoffReceipt { + readonly handoffId: string; + readonly targetAgentId: string; + readonly status: 'queued' | 'accepted'; + readonly correlationId: string; +} + +export interface MosCoordinationActivity { + readonly occurredAt: string; + readonly status: MosHandoffStatus; + readonly summary: string; +} + +export interface CoordinationObservation { + readonly handoffId: string; + readonly targetAgentId: string; + readonly status: MosHandoffStatus; + readonly correlationId: string; + readonly activity: readonly MosCoordinationActivity[]; +} + +export interface CoordinationResult { + readonly handoffId: string; + readonly targetAgentId: string; + readonly status: 'completed' | 'failed' | 'pending'; + readonly correlationId: string; + readonly summary?: string; +} + +/** + * Transport-neutral boundary. The interaction plane can request work and read + * its progress/result, but it cannot issue worker, review, merge, or other + * general orchestration commands. + */ +export interface MosCoordinationPort { + handoff(handoff: MosHandoff): Promise; + observe(handoffId: string, scope: CoordinationScope): Promise; + result(handoffId: string, scope: CoordinationScope): Promise; +} + +export type MosCoordinationAuthorityErrorCode = + | 'invalid_identity' + | 'requester_forbidden' + | 'target_drift' + | 'correlation_drift'; + +export class MosCoordinationAuthorityError extends Error { + constructor( + readonly code: MosCoordinationAuthorityErrorCode, + message: string, + ) { + super(message); + this.name = MosCoordinationAuthorityError.name; + } +} + +/** + * Enforces the interaction-to-orchestration authority boundary before a + * transport is reached. Identity names remain configuration data. + */ +export class MosCoordinationClient { + private readonly identity: MosCoordinationIdentity; + + constructor( + identity: MosCoordinationIdentity, + private readonly port: MosCoordinationPort, + private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(), + ) { + this.identity = normalizeIdentity(identity); + } + + async handoff(request: MosHandoffRequest, scope: CoordinationScope): Promise { + this.assertRequester(scope); + const handoff: MosHandoff = { + handoffId: this.handoffIdFactory(), + targetAgentId: this.identity.orchestrationAgentId, + request: snapshotRequest(request), + scope: snapshotScope(scope), + }; + const receipt = await this.port.handoff(handoff); + if ( + receipt.handoffId !== handoff.handoffId || + receipt.targetAgentId !== handoff.targetAgentId + ) { + throw new MosCoordinationAuthorityError( + 'target_drift', + 'Mos coordination transport returned a mismatched handoff target', + ); + } + if (receipt.correlationId !== handoff.scope.correlationId) { + throw new MosCoordinationAuthorityError( + 'correlation_drift', + 'Mos coordination transport returned a mismatched correlation ID', + ); + } + return receipt; + } + + async observe(handoffId: string, scope: CoordinationScope): Promise { + this.assertRequester(scope); + return this.assertObservation(await this.port.observe(handoffId, snapshotScope(scope)), scope); + } + + async result(handoffId: string, scope: CoordinationScope): Promise { + this.assertRequester(scope); + return this.assertResult(await this.port.result(handoffId, snapshotScope(scope)), scope); + } + + private assertRequester(scope: CoordinationScope): void { + if (scope.requesterAgentId !== this.identity.interactionAgentId) { + throw new MosCoordinationAuthorityError( + 'requester_forbidden', + 'Requester is not the configured interaction agent', + ); + } + } + + private assertObservation( + observation: CoordinationObservation, + scope: CoordinationScope, + ): CoordinationObservation { + if (observation.targetAgentId !== this.identity.orchestrationAgentId) { + throw new MosCoordinationAuthorityError( + 'target_drift', + 'Mos coordination transport returned an unexpected observation target', + ); + } + if (observation.correlationId !== scope.correlationId) { + throw new MosCoordinationAuthorityError( + 'correlation_drift', + 'Mos coordination transport returned a mismatched observation correlation ID', + ); + } + return observation; + } + + private assertResult(result: CoordinationResult, scope: CoordinationScope): CoordinationResult { + if (result.targetAgentId !== this.identity.orchestrationAgentId) { + throw new MosCoordinationAuthorityError( + 'target_drift', + 'Mos coordination transport returned an unexpected result target', + ); + } + if (result.correlationId !== scope.correlationId) { + throw new MosCoordinationAuthorityError( + 'correlation_drift', + 'Mos coordination transport returned a mismatched result correlation ID', + ); + } + return result; + } +} + +function normalizeIdentity(identity: MosCoordinationIdentity): MosCoordinationIdentity { + const interactionAgentId = identity.interactionAgentId.trim(); + const orchestrationAgentId = identity.orchestrationAgentId.trim(); + if (interactionAgentId.length === 0 || orchestrationAgentId.length === 0) { + throw new MosCoordinationAuthorityError( + 'invalid_identity', + 'Interaction and orchestration identities are required', + ); + } + if (interactionAgentId === orchestrationAgentId) { + throw new MosCoordinationAuthorityError( + 'invalid_identity', + 'Interaction and orchestration identities must differ', + ); + } + return Object.freeze({ interactionAgentId, orchestrationAgentId }); +} + +function snapshotRequest(request: MosHandoffRequest): MosHandoffRequest { + return Object.freeze({ ...request }); +} + +function snapshotScope(scope: CoordinationScope): CoordinationScope { + return Object.freeze({ ...scope }); +} diff --git a/packages/db/drizzle/0012_interaction_durable_state.sql b/packages/db/drizzle/0012_interaction_durable_state.sql new file mode 100644 index 0000000..36d8680 --- /dev/null +++ b/packages/db/drizzle/0012_interaction_durable_state.sql @@ -0,0 +1,71 @@ +CREATE TYPE "public"."interaction_handoff_status" AS ENUM('pending', 'accepted');--> statement-breakpoint +CREATE TYPE "public"."interaction_inbox_status" AS ENUM('pending', 'processing', 'processed');--> statement-breakpoint +CREATE TYPE "public"."interaction_outbox_status" AS ENUM('pending', 'processing', 'delivered');--> statement-breakpoint +CREATE TABLE "interaction_checkpoints" ( + "session_id" text PRIMARY KEY NOT NULL, + "checkpoint_id" text NOT NULL, + "cursor" text NOT NULL, + "summary" text NOT NULL, + "compaction_epoch" integer NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "interaction_checkpoints_checkpoint_id_unique" UNIQUE("checkpoint_id") +); +--> statement-breakpoint +CREATE TABLE "interaction_handoffs" ( + "handoff_id" text PRIMARY KEY NOT NULL, + "session_id" text NOT NULL, + "destination" text NOT NULL, + "correlation_id" text NOT NULL, + "checkpoint_id" text NOT NULL, + "status" "interaction_handoff_status" DEFAULT 'pending' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "interaction_inbox" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "session_id" text NOT NULL, + "idempotency_key" text NOT NULL, + "correlation_id" text NOT NULL, + "content" text NOT NULL, + "content_digest" text NOT NULL, + "status" "interaction_inbox_status" DEFAULT 'pending' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "interaction_outbox" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "session_id" text NOT NULL, + "idempotency_key" text NOT NULL, + "correlation_id" text NOT NULL, + "kind" text NOT NULL, + "content" text NOT NULL, + "content_digest" text NOT NULL, + "status" "interaction_outbox_status" DEFAULT 'pending' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "interaction_sessions" ( + "id" text PRIMARY KEY NOT NULL, + "agent_name" text NOT NULL, + "tenant_id" text NOT NULL, + "owner_id" text NOT NULL, + "provider_id" text NOT NULL, + "runtime_session_id" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "interaction_checkpoints" ADD CONSTRAINT "interaction_checkpoints_session_id_interaction_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."interaction_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "interaction_handoffs" ADD CONSTRAINT "interaction_handoffs_session_id_interaction_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."interaction_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "interaction_inbox" ADD CONSTRAINT "interaction_inbox_session_id_interaction_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."interaction_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "interaction_outbox" ADD CONSTRAINT "interaction_outbox_session_id_interaction_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."interaction_sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "interaction_sessions" ADD CONSTRAINT "interaction_sessions_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "interaction_handoffs_session_status_idx" ON "interaction_handoffs" USING btree ("session_id","status");--> statement-breakpoint +CREATE UNIQUE INDEX "interaction_inbox_session_idempotency_idx" ON "interaction_inbox" USING btree ("session_id","idempotency_key");--> statement-breakpoint +CREATE INDEX "interaction_inbox_session_status_created_idx" ON "interaction_inbox" USING btree ("session_id","status","created_at");--> statement-breakpoint +CREATE UNIQUE INDEX "interaction_outbox_session_idempotency_idx" ON "interaction_outbox" USING btree ("session_id","idempotency_key");--> statement-breakpoint +CREATE INDEX "interaction_outbox_session_status_created_idx" ON "interaction_outbox" USING btree ("session_id","status","created_at"); \ No newline at end of file diff --git a/packages/db/drizzle/0013_interaction_checkpoint_history.sql b/packages/db/drizzle/0013_interaction_checkpoint_history.sql new file mode 100644 index 0000000..7fc8573 --- /dev/null +++ b/packages/db/drizzle/0013_interaction_checkpoint_history.sql @@ -0,0 +1,5 @@ +ALTER TABLE "interaction_checkpoints" DROP CONSTRAINT "interaction_checkpoints_checkpoint_id_unique";--> statement-breakpoint +ALTER TABLE "interaction_checkpoints" DROP CONSTRAINT "interaction_checkpoints_pkey";--> statement-breakpoint +ALTER TABLE "interaction_checkpoints" ADD COLUMN "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "interaction_checkpoints_session_idempotency_idx" ON "interaction_checkpoints" USING btree ("session_id","checkpoint_id");--> statement-breakpoint +CREATE INDEX "interaction_checkpoints_session_epoch_idx" ON "interaction_checkpoints" USING btree ("session_id","compaction_epoch"); \ No newline at end of file diff --git a/packages/db/drizzle/0014_interaction_outbox_channel_scope.sql b/packages/db/drizzle/0014_interaction_outbox_channel_scope.sql new file mode 100644 index 0000000..af44cf5 --- /dev/null +++ b/packages/db/drizzle/0014_interaction_outbox_channel_scope.sql @@ -0,0 +1,5 @@ +ALTER TABLE "interaction_outbox" ADD COLUMN "channel_id" text; +--> statement-breakpoint +UPDATE "interaction_outbox" SET "channel_id" = 'legacy:unknown' WHERE "channel_id" IS NULL; +--> statement-breakpoint +ALTER TABLE "interaction_outbox" ALTER COLUMN "channel_id" SET NOT NULL; diff --git a/packages/db/drizzle/0015_interaction_checkpoint_payload_digest.sql b/packages/db/drizzle/0015_interaction_checkpoint_payload_digest.sql new file mode 100644 index 0000000..f291de1 --- /dev/null +++ b/packages/db/drizzle/0015_interaction_checkpoint_payload_digest.sql @@ -0,0 +1,3 @@ +ALTER TABLE "interaction_checkpoints" ADD COLUMN "content_digest" text;--> statement-breakpoint +UPDATE "interaction_checkpoints" SET "content_digest" = 'legacy' WHERE "content_digest" IS NULL;--> statement-breakpoint +ALTER TABLE "interaction_checkpoints" ALTER COLUMN "content_digest" SET NOT NULL; diff --git a/packages/db/drizzle/meta/0012_snapshot.json b/packages/db/drizzle/meta/0012_snapshot.json new file mode 100644 index 0000000..721391c --- /dev/null +++ b/packages/db/drizzle/meta/0012_snapshot.json @@ -0,0 +1,4172 @@ +{ + "id": "5d2dbfe9-f5d2-4342-a613-8c73943a6122", + "prevId": "0aa37ae4-5a0b-464b-ba70-121c5d9bbd23", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_account_idx": { + "name": "accounts_provider_account_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_user_id_idx": { + "name": "accounts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_tokens": { + "name": "admin_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_tokens_user_id_idx": { + "name": "admin_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_tokens_hash_idx": { + "name": "admin_tokens_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_tokens_user_id_users_id_fk": { + "name": "admin_tokens_user_id_users_id_fk", + "tableFrom": "admin_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_logs": { + "name": "agent_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'hot'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "summarized_at": { + "name": "summarized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_logs_session_tier_idx": { + "name": "agent_logs_session_tier_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_user_id_idx": { + "name": "agent_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_tier_created_at_idx": { + "name": "agent_logs_tier_created_at_idx", + "columns": [ + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_logs_user_id_users_id_fk": { + "name": "agent_logs_user_id_users_id_fk", + "tableFrom": "agent_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_project_id_idx": { + "name": "agents_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_owner_id_idx": { + "name": "agents_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_is_system_idx": { + "name": "agents_is_system_idx", + "columns": [ + { + "expression": "is_system", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_project_id_projects_id_fk": { + "name": "agents_project_id_projects_id_fk", + "tableFrom": "agents", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_owner_id_users_id_fk": { + "name": "agents_owner_id_users_id_fk", + "tableFrom": "agents", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.appreciations": { + "name": "appreciations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "from_user": { + "name": "from_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_user": { + "name": "to_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backlog": { + "name": "backlog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "backlog_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_ttl_seconds": { + "name": "claim_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptance": { + "name": "acceptance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "backlog_status_priority_idx": { + "name": "backlog_status_priority_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backlog_status_claimed_at_idx": { + "name": "backlog_status_claimed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backlog_idempotency_key_idx": { + "name": "backlog_idempotency_key_idx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conversations_user_archived_idx": { + "name": "conversations_user_archived_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_project_id_idx": { + "name": "conversations_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_agent_id_idx": { + "name": "conversations_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversations_user_id_users_id_fk": { + "name": "conversations_user_id_users_id_fk", + "tableFrom": "conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_project_id_projects_id_fk": { + "name": "conversations_project_id_projects_id_fk", + "tableFrom": "conversations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_agent_id_agents_id_fk": { + "name": "conversations_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "events_type_idx": { + "name": "events_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_date_idx": { + "name": "events_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_audit_log": { + "name": "federation_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "verb": { + "name": "verb", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "denied_reason": { + "name": "denied_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "query_hash": { + "name": "query_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bytes_out": { + "name": "bytes_out", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_audit_log_peer_created_at_idx": { + "name": "federation_audit_log_peer_created_at_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_subject_created_at_idx": { + "name": "federation_audit_log_subject_created_at_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_created_at_idx": { + "name": "federation_audit_log_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_audit_log_peer_id_federation_peers_id_fk": { + "name": "federation_audit_log_peer_id_federation_peers_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_subject_user_id_users_id_fk": { + "name": "federation_audit_log_subject_user_id_users_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_grant_id_federation_grants_id_fk": { + "name": "federation_audit_log_grant_id_federation_grants_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_enrollment_tokens": { + "name": "federation_enrollment_tokens", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federation_enrollment_tokens_grant_id_federation_grants_id_fk": { + "name": "federation_enrollment_tokens_grant_id_federation_grants_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_enrollment_tokens_peer_id_federation_peers_id_fk": { + "name": "federation_enrollment_tokens_peer_id_federation_peers_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_grants": { + "name": "federation_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "grant_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_reason": { + "name": "revoked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_grants_subject_status_idx": { + "name": "federation_grants_subject_status_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_grants_peer_status_idx": { + "name": "federation_grants_peer_status_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_grants_subject_user_id_users_id_fk": { + "name": "federation_grants_subject_user_id_users_id_fk", + "tableFrom": "federation_grants", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_grants_peer_id_federation_peers_id_fk": { + "name": "federation_grants_peer_id_federation_peers_id_fk", + "tableFrom": "federation_grants", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_peers": { + "name": "federation_peers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "common_name": { + "name": "common_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_serial": { + "name": "cert_serial", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_not_after": { + "name": "cert_not_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "client_key_pem": { + "name": "client_key_pem", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "peer_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "endpoint_url": { + "name": "endpoint_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_peers_cert_serial_idx": { + "name": "federation_peers_cert_serial_idx", + "columns": [ + { + "expression": "cert_serial", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_peers_state_idx": { + "name": "federation_peers_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federation_peers_common_name_unique": { + "name": "federation_peers_common_name_unique", + "nullsNotDistinct": false, + "columns": [ + "common_name" + ] + }, + "federation_peers_cert_serial_unique": { + "name": "federation_peers_cert_serial_unique", + "nullsNotDistinct": false, + "columns": [ + "cert_serial" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.insights": { + "name": "insights", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'agent'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "relevance_score": { + "name": "relevance_score", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decayed_at": { + "name": "decayed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "insights_user_id_idx": { + "name": "insights_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_category_idx": { + "name": "insights_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_relevance_idx": { + "name": "insights_relevance_idx", + "columns": [ + { + "expression": "relevance_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "insights_user_id_users_id_fk": { + "name": "insights_user_id_users_id_fk", + "tableFrom": "insights", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "messages_conversation_id_idx": { + "name": "messages_conversation_id_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mission_tasks": { + "name": "mission_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr": { + "name": "pr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mission_tasks_mission_id_idx": { + "name": "mission_tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_task_id_idx": { + "name": "mission_tasks_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_user_id_idx": { + "name": "mission_tasks_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_status_idx": { + "name": "mission_tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mission_tasks_mission_id_missions_id_fk": { + "name": "mission_tasks_mission_id_missions_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mission_tasks_task_id_tasks_id_fk": { + "name": "mission_tasks_task_id_tasks_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mission_tasks_user_id_users_id_fk": { + "name": "mission_tasks_user_id_users_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions": { + "name": "missions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planning'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "milestones": { + "name": "milestones", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "missions_project_id_idx": { + "name": "missions_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_user_id_idx": { + "name": "missions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_project_id_projects_id_fk": { + "name": "missions_project_id_projects_id_fk", + "tableFrom": "missions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_user_id_users_id_fk": { + "name": "missions_user_id_users_id_fk", + "tableFrom": "missions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preferences": { + "name": "preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mutable": { + "name": "mutable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "preferences_user_id_idx": { + "name": "preferences_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "preferences_user_key_idx": { + "name": "preferences_user_key_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "preferences_user_id_users_id_fk": { + "name": "preferences_user_id_users_id_fk", + "tableFrom": "preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_owner_id_users_id_fk": { + "name": "projects_owner_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "projects_team_id_teams_id_fk": { + "name": "projects_team_id_teams_id_fk", + "tableFrom": "projects", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_credentials": { + "name": "provider_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_type": { + "name": "credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "provider_credentials_user_provider_idx": { + "name": "provider_credentials_user_provider_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_credentials_user_id_idx": { + "name": "provider_credentials_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_credentials_user_id_users_id_fk": { + "name": "provider_credentials_user_id_users_id_fk", + "tableFrom": "provider_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routing_rules": { + "name": "routing_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routing_rules_scope_priority_idx": { + "name": "routing_rules_scope_priority_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_user_id_idx": { + "name": "routing_rules_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_enabled_idx": { + "name": "routing_rules_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routing_rules_user_id_users_id_fk": { + "name": "routing_rules_user_id_users_id_fk", + "tableFrom": "routing_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "installed_by": { + "name": "installed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skills_enabled_idx": { + "name": "skills_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skills_installed_by_users_id_fk": { + "name": "skills_installed_by_users_id_fk", + "tableFrom": "skills", + "tableTo": "users", + "columnsFrom": [ + "installed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "skills_name_unique": { + "name": "skills_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.summarization_jobs": { + "name": "summarization_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "logs_processed": { + "name": "logs_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "insights_created": { + "name": "insights_created", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "summarization_jobs_status_idx": { + "name": "summarization_jobs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee": { + "name": "assignee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_project_id_idx": { + "name": "tasks_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_mission_id_idx": { + "name": "tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_project_id_projects_id_fk": { + "name": "tasks_project_id_projects_id_fk", + "tableFrom": "tasks", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_mission_id_missions_id_fk": { + "name": "tasks_mission_id_missions_id_fk", + "tableFrom": "tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_members_team_user_idx": { + "name": "team_members_team_user_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_invited_by_users_id_fk": { + "name": "team_members_invited_by_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "teams_manager_id_users_id_fk": { + "name": "teams_manager_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_slug_unique": { + "name": "teams_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_checkpoints": { + "name": "interaction_checkpoints", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "compaction_epoch": { + "name": "compaction_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "interaction_checkpoints_session_id_interaction_sessions_id_fk": { + "name": "interaction_checkpoints_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_checkpoints", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "interaction_checkpoints_checkpoint_id_unique": { + "name": "interaction_checkpoints_checkpoint_id_unique", + "nullsNotDistinct": false, + "columns": [ + "checkpoint_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_handoffs": { + "name": "interaction_handoffs", + "schema": "", + "columns": { + "handoff_id": { + "name": "handoff_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_handoff_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_handoffs_session_status_idx": { + "name": "interaction_handoffs_session_status_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_handoffs_session_id_interaction_sessions_id_fk": { + "name": "interaction_handoffs_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_handoffs", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_inbox": { + "name": "interaction_inbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_inbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_inbox_session_idempotency_idx": { + "name": "interaction_inbox_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_inbox_session_status_created_idx": { + "name": "interaction_inbox_session_status_created_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_inbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_inbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_inbox", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_outbox": { + "name": "interaction_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_outbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_outbox_session_idempotency_idx": { + "name": "interaction_outbox_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_outbox_session_status_created_idx": { + "name": "interaction_outbox_session_status_created_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_outbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_outbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_outbox", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_sessions": { + "name": "interaction_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime_session_id": { + "name": "runtime_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "interaction_sessions_owner_id_users_id_fk": { + "name": "interaction_sessions_owner_id_users_id_fk", + "tableFrom": "interaction_sessions", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tickets": { + "name": "tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tickets_status_idx": { + "name": "tickets_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.backlog_status": { + "name": "backlog_status", + "schema": "public", + "values": [ + "ready", + "claimed", + "blocked", + "done" + ] + }, + "public.grant_status": { + "name": "grant_status", + "schema": "public", + "values": [ + "pending", + "active", + "revoked", + "expired" + ] + }, + "public.peer_state": { + "name": "peer_state", + "schema": "public", + "values": [ + "pending", + "active", + "suspended", + "revoked" + ] + }, + "public.interaction_handoff_status": { + "name": "interaction_handoff_status", + "schema": "public", + "values": [ + "pending", + "accepted" + ] + }, + "public.interaction_inbox_status": { + "name": "interaction_inbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "processed" + ] + }, + "public.interaction_outbox_status": { + "name": "interaction_outbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "delivered" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/0013_snapshot.json b/packages/db/drizzle/meta/0013_snapshot.json new file mode 100644 index 0000000..6c85c86 --- /dev/null +++ b/packages/db/drizzle/meta/0013_snapshot.json @@ -0,0 +1,4214 @@ +{ + "id": "0cd8e1a3-a7f6-4c39-ac19-7cc0f9c02164", + "prevId": "5d2dbfe9-f5d2-4342-a613-8c73943a6122", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_account_idx": { + "name": "accounts_provider_account_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_user_id_idx": { + "name": "accounts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_tokens": { + "name": "admin_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_tokens_user_id_idx": { + "name": "admin_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_tokens_hash_idx": { + "name": "admin_tokens_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_tokens_user_id_users_id_fk": { + "name": "admin_tokens_user_id_users_id_fk", + "tableFrom": "admin_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_logs": { + "name": "agent_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'hot'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "summarized_at": { + "name": "summarized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_logs_session_tier_idx": { + "name": "agent_logs_session_tier_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_user_id_idx": { + "name": "agent_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_tier_created_at_idx": { + "name": "agent_logs_tier_created_at_idx", + "columns": [ + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_logs_user_id_users_id_fk": { + "name": "agent_logs_user_id_users_id_fk", + "tableFrom": "agent_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_project_id_idx": { + "name": "agents_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_owner_id_idx": { + "name": "agents_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_is_system_idx": { + "name": "agents_is_system_idx", + "columns": [ + { + "expression": "is_system", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_project_id_projects_id_fk": { + "name": "agents_project_id_projects_id_fk", + "tableFrom": "agents", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_owner_id_users_id_fk": { + "name": "agents_owner_id_users_id_fk", + "tableFrom": "agents", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.appreciations": { + "name": "appreciations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "from_user": { + "name": "from_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_user": { + "name": "to_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backlog": { + "name": "backlog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "backlog_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_ttl_seconds": { + "name": "claim_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptance": { + "name": "acceptance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "backlog_status_priority_idx": { + "name": "backlog_status_priority_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backlog_status_claimed_at_idx": { + "name": "backlog_status_claimed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backlog_idempotency_key_idx": { + "name": "backlog_idempotency_key_idx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conversations_user_archived_idx": { + "name": "conversations_user_archived_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_project_id_idx": { + "name": "conversations_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_agent_id_idx": { + "name": "conversations_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversations_user_id_users_id_fk": { + "name": "conversations_user_id_users_id_fk", + "tableFrom": "conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_project_id_projects_id_fk": { + "name": "conversations_project_id_projects_id_fk", + "tableFrom": "conversations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_agent_id_agents_id_fk": { + "name": "conversations_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "events_type_idx": { + "name": "events_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_date_idx": { + "name": "events_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_audit_log": { + "name": "federation_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "verb": { + "name": "verb", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "denied_reason": { + "name": "denied_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "query_hash": { + "name": "query_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bytes_out": { + "name": "bytes_out", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_audit_log_peer_created_at_idx": { + "name": "federation_audit_log_peer_created_at_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_subject_created_at_idx": { + "name": "federation_audit_log_subject_created_at_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_created_at_idx": { + "name": "federation_audit_log_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_audit_log_peer_id_federation_peers_id_fk": { + "name": "federation_audit_log_peer_id_federation_peers_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_subject_user_id_users_id_fk": { + "name": "federation_audit_log_subject_user_id_users_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_grant_id_federation_grants_id_fk": { + "name": "federation_audit_log_grant_id_federation_grants_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_enrollment_tokens": { + "name": "federation_enrollment_tokens", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federation_enrollment_tokens_grant_id_federation_grants_id_fk": { + "name": "federation_enrollment_tokens_grant_id_federation_grants_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_enrollment_tokens_peer_id_federation_peers_id_fk": { + "name": "federation_enrollment_tokens_peer_id_federation_peers_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_grants": { + "name": "federation_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "grant_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_reason": { + "name": "revoked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_grants_subject_status_idx": { + "name": "federation_grants_subject_status_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_grants_peer_status_idx": { + "name": "federation_grants_peer_status_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_grants_subject_user_id_users_id_fk": { + "name": "federation_grants_subject_user_id_users_id_fk", + "tableFrom": "federation_grants", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_grants_peer_id_federation_peers_id_fk": { + "name": "federation_grants_peer_id_federation_peers_id_fk", + "tableFrom": "federation_grants", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_peers": { + "name": "federation_peers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "common_name": { + "name": "common_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_serial": { + "name": "cert_serial", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_not_after": { + "name": "cert_not_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "client_key_pem": { + "name": "client_key_pem", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "peer_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "endpoint_url": { + "name": "endpoint_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_peers_cert_serial_idx": { + "name": "federation_peers_cert_serial_idx", + "columns": [ + { + "expression": "cert_serial", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_peers_state_idx": { + "name": "federation_peers_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federation_peers_common_name_unique": { + "name": "federation_peers_common_name_unique", + "nullsNotDistinct": false, + "columns": [ + "common_name" + ] + }, + "federation_peers_cert_serial_unique": { + "name": "federation_peers_cert_serial_unique", + "nullsNotDistinct": false, + "columns": [ + "cert_serial" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.insights": { + "name": "insights", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'agent'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "relevance_score": { + "name": "relevance_score", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decayed_at": { + "name": "decayed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "insights_user_id_idx": { + "name": "insights_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_category_idx": { + "name": "insights_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_relevance_idx": { + "name": "insights_relevance_idx", + "columns": [ + { + "expression": "relevance_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "insights_user_id_users_id_fk": { + "name": "insights_user_id_users_id_fk", + "tableFrom": "insights", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "messages_conversation_id_idx": { + "name": "messages_conversation_id_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mission_tasks": { + "name": "mission_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr": { + "name": "pr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mission_tasks_mission_id_idx": { + "name": "mission_tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_task_id_idx": { + "name": "mission_tasks_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_user_id_idx": { + "name": "mission_tasks_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_status_idx": { + "name": "mission_tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mission_tasks_mission_id_missions_id_fk": { + "name": "mission_tasks_mission_id_missions_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mission_tasks_task_id_tasks_id_fk": { + "name": "mission_tasks_task_id_tasks_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mission_tasks_user_id_users_id_fk": { + "name": "mission_tasks_user_id_users_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions": { + "name": "missions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planning'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "milestones": { + "name": "milestones", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "missions_project_id_idx": { + "name": "missions_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_user_id_idx": { + "name": "missions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_project_id_projects_id_fk": { + "name": "missions_project_id_projects_id_fk", + "tableFrom": "missions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_user_id_users_id_fk": { + "name": "missions_user_id_users_id_fk", + "tableFrom": "missions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preferences": { + "name": "preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mutable": { + "name": "mutable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "preferences_user_id_idx": { + "name": "preferences_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "preferences_user_key_idx": { + "name": "preferences_user_key_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "preferences_user_id_users_id_fk": { + "name": "preferences_user_id_users_id_fk", + "tableFrom": "preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_owner_id_users_id_fk": { + "name": "projects_owner_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "projects_team_id_teams_id_fk": { + "name": "projects_team_id_teams_id_fk", + "tableFrom": "projects", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_credentials": { + "name": "provider_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_type": { + "name": "credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "provider_credentials_user_provider_idx": { + "name": "provider_credentials_user_provider_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_credentials_user_id_idx": { + "name": "provider_credentials_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_credentials_user_id_users_id_fk": { + "name": "provider_credentials_user_id_users_id_fk", + "tableFrom": "provider_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routing_rules": { + "name": "routing_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routing_rules_scope_priority_idx": { + "name": "routing_rules_scope_priority_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_user_id_idx": { + "name": "routing_rules_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_enabled_idx": { + "name": "routing_rules_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routing_rules_user_id_users_id_fk": { + "name": "routing_rules_user_id_users_id_fk", + "tableFrom": "routing_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "installed_by": { + "name": "installed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skills_enabled_idx": { + "name": "skills_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skills_installed_by_users_id_fk": { + "name": "skills_installed_by_users_id_fk", + "tableFrom": "skills", + "tableTo": "users", + "columnsFrom": [ + "installed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "skills_name_unique": { + "name": "skills_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.summarization_jobs": { + "name": "summarization_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "logs_processed": { + "name": "logs_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "insights_created": { + "name": "insights_created", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "summarization_jobs_status_idx": { + "name": "summarization_jobs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee": { + "name": "assignee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_project_id_idx": { + "name": "tasks_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_mission_id_idx": { + "name": "tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_project_id_projects_id_fk": { + "name": "tasks_project_id_projects_id_fk", + "tableFrom": "tasks", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_mission_id_missions_id_fk": { + "name": "tasks_mission_id_missions_id_fk", + "tableFrom": "tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_members_team_user_idx": { + "name": "team_members_team_user_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_invited_by_users_id_fk": { + "name": "team_members_invited_by_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "teams_manager_id_users_id_fk": { + "name": "teams_manager_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_slug_unique": { + "name": "teams_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_checkpoints": { + "name": "interaction_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "compaction_epoch": { + "name": "compaction_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_checkpoints_session_idempotency_idx": { + "name": "interaction_checkpoints_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_checkpoints_session_epoch_idx": { + "name": "interaction_checkpoints_session_epoch_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "compaction_epoch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_checkpoints_session_id_interaction_sessions_id_fk": { + "name": "interaction_checkpoints_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_checkpoints", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_handoffs": { + "name": "interaction_handoffs", + "schema": "", + "columns": { + "handoff_id": { + "name": "handoff_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_handoff_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_handoffs_session_status_idx": { + "name": "interaction_handoffs_session_status_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_handoffs_session_id_interaction_sessions_id_fk": { + "name": "interaction_handoffs_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_handoffs", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_inbox": { + "name": "interaction_inbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_inbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_inbox_session_idempotency_idx": { + "name": "interaction_inbox_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_inbox_session_status_created_idx": { + "name": "interaction_inbox_session_status_created_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_inbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_inbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_inbox", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_outbox": { + "name": "interaction_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_outbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_outbox_session_idempotency_idx": { + "name": "interaction_outbox_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_outbox_session_status_created_idx": { + "name": "interaction_outbox_session_status_created_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_outbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_outbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_outbox", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_sessions": { + "name": "interaction_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime_session_id": { + "name": "runtime_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "interaction_sessions_owner_id_users_id_fk": { + "name": "interaction_sessions_owner_id_users_id_fk", + "tableFrom": "interaction_sessions", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tickets": { + "name": "tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tickets_status_idx": { + "name": "tickets_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.backlog_status": { + "name": "backlog_status", + "schema": "public", + "values": [ + "ready", + "claimed", + "blocked", + "done" + ] + }, + "public.grant_status": { + "name": "grant_status", + "schema": "public", + "values": [ + "pending", + "active", + "revoked", + "expired" + ] + }, + "public.peer_state": { + "name": "peer_state", + "schema": "public", + "values": [ + "pending", + "active", + "suspended", + "revoked" + ] + }, + "public.interaction_handoff_status": { + "name": "interaction_handoff_status", + "schema": "public", + "values": [ + "pending", + "accepted" + ] + }, + "public.interaction_inbox_status": { + "name": "interaction_inbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "processed" + ] + }, + "public.interaction_outbox_status": { + "name": "interaction_outbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "delivered" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/0014_snapshot.json b/packages/db/drizzle/meta/0014_snapshot.json new file mode 100644 index 0000000..b60ca23 --- /dev/null +++ b/packages/db/drizzle/meta/0014_snapshot.json @@ -0,0 +1,4220 @@ +{ + "id": "48124a40-a224-4d1c-ab8f-b3dab554b1ac", + "prevId": "0cd8e1a3-a7f6-4c39-ac19-7cc0f9c02164", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_account_idx": { + "name": "accounts_provider_account_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_user_id_idx": { + "name": "accounts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_tokens": { + "name": "admin_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_tokens_user_id_idx": { + "name": "admin_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_tokens_hash_idx": { + "name": "admin_tokens_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_tokens_user_id_users_id_fk": { + "name": "admin_tokens_user_id_users_id_fk", + "tableFrom": "admin_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_logs": { + "name": "agent_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'hot'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "summarized_at": { + "name": "summarized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_logs_session_tier_idx": { + "name": "agent_logs_session_tier_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_user_id_idx": { + "name": "agent_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_tier_created_at_idx": { + "name": "agent_logs_tier_created_at_idx", + "columns": [ + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_logs_user_id_users_id_fk": { + "name": "agent_logs_user_id_users_id_fk", + "tableFrom": "agent_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_project_id_idx": { + "name": "agents_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_owner_id_idx": { + "name": "agents_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_is_system_idx": { + "name": "agents_is_system_idx", + "columns": [ + { + "expression": "is_system", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_project_id_projects_id_fk": { + "name": "agents_project_id_projects_id_fk", + "tableFrom": "agents", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_owner_id_users_id_fk": { + "name": "agents_owner_id_users_id_fk", + "tableFrom": "agents", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.appreciations": { + "name": "appreciations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "from_user": { + "name": "from_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_user": { + "name": "to_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backlog": { + "name": "backlog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "backlog_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_ttl_seconds": { + "name": "claim_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptance": { + "name": "acceptance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "backlog_status_priority_idx": { + "name": "backlog_status_priority_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backlog_status_claimed_at_idx": { + "name": "backlog_status_claimed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backlog_idempotency_key_idx": { + "name": "backlog_idempotency_key_idx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conversations_user_archived_idx": { + "name": "conversations_user_archived_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_project_id_idx": { + "name": "conversations_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_agent_id_idx": { + "name": "conversations_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversations_user_id_users_id_fk": { + "name": "conversations_user_id_users_id_fk", + "tableFrom": "conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_project_id_projects_id_fk": { + "name": "conversations_project_id_projects_id_fk", + "tableFrom": "conversations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_agent_id_agents_id_fk": { + "name": "conversations_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "events_type_idx": { + "name": "events_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_date_idx": { + "name": "events_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_audit_log": { + "name": "federation_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "verb": { + "name": "verb", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "denied_reason": { + "name": "denied_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "query_hash": { + "name": "query_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bytes_out": { + "name": "bytes_out", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_audit_log_peer_created_at_idx": { + "name": "federation_audit_log_peer_created_at_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_subject_created_at_idx": { + "name": "federation_audit_log_subject_created_at_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_created_at_idx": { + "name": "federation_audit_log_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_audit_log_peer_id_federation_peers_id_fk": { + "name": "federation_audit_log_peer_id_federation_peers_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_subject_user_id_users_id_fk": { + "name": "federation_audit_log_subject_user_id_users_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_grant_id_federation_grants_id_fk": { + "name": "federation_audit_log_grant_id_federation_grants_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_enrollment_tokens": { + "name": "federation_enrollment_tokens", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federation_enrollment_tokens_grant_id_federation_grants_id_fk": { + "name": "federation_enrollment_tokens_grant_id_federation_grants_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_enrollment_tokens_peer_id_federation_peers_id_fk": { + "name": "federation_enrollment_tokens_peer_id_federation_peers_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_grants": { + "name": "federation_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "grant_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_reason": { + "name": "revoked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_grants_subject_status_idx": { + "name": "federation_grants_subject_status_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_grants_peer_status_idx": { + "name": "federation_grants_peer_status_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_grants_subject_user_id_users_id_fk": { + "name": "federation_grants_subject_user_id_users_id_fk", + "tableFrom": "federation_grants", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_grants_peer_id_federation_peers_id_fk": { + "name": "federation_grants_peer_id_federation_peers_id_fk", + "tableFrom": "federation_grants", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_peers": { + "name": "federation_peers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "common_name": { + "name": "common_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_serial": { + "name": "cert_serial", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_not_after": { + "name": "cert_not_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "client_key_pem": { + "name": "client_key_pem", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "peer_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "endpoint_url": { + "name": "endpoint_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_peers_cert_serial_idx": { + "name": "federation_peers_cert_serial_idx", + "columns": [ + { + "expression": "cert_serial", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_peers_state_idx": { + "name": "federation_peers_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federation_peers_common_name_unique": { + "name": "federation_peers_common_name_unique", + "nullsNotDistinct": false, + "columns": [ + "common_name" + ] + }, + "federation_peers_cert_serial_unique": { + "name": "federation_peers_cert_serial_unique", + "nullsNotDistinct": false, + "columns": [ + "cert_serial" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.insights": { + "name": "insights", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'agent'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "relevance_score": { + "name": "relevance_score", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decayed_at": { + "name": "decayed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "insights_user_id_idx": { + "name": "insights_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_category_idx": { + "name": "insights_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_relevance_idx": { + "name": "insights_relevance_idx", + "columns": [ + { + "expression": "relevance_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "insights_user_id_users_id_fk": { + "name": "insights_user_id_users_id_fk", + "tableFrom": "insights", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "messages_conversation_id_idx": { + "name": "messages_conversation_id_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mission_tasks": { + "name": "mission_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr": { + "name": "pr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mission_tasks_mission_id_idx": { + "name": "mission_tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_task_id_idx": { + "name": "mission_tasks_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_user_id_idx": { + "name": "mission_tasks_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_status_idx": { + "name": "mission_tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mission_tasks_mission_id_missions_id_fk": { + "name": "mission_tasks_mission_id_missions_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mission_tasks_task_id_tasks_id_fk": { + "name": "mission_tasks_task_id_tasks_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mission_tasks_user_id_users_id_fk": { + "name": "mission_tasks_user_id_users_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions": { + "name": "missions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planning'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "milestones": { + "name": "milestones", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "missions_project_id_idx": { + "name": "missions_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_user_id_idx": { + "name": "missions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_project_id_projects_id_fk": { + "name": "missions_project_id_projects_id_fk", + "tableFrom": "missions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_user_id_users_id_fk": { + "name": "missions_user_id_users_id_fk", + "tableFrom": "missions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preferences": { + "name": "preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mutable": { + "name": "mutable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "preferences_user_id_idx": { + "name": "preferences_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "preferences_user_key_idx": { + "name": "preferences_user_key_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "preferences_user_id_users_id_fk": { + "name": "preferences_user_id_users_id_fk", + "tableFrom": "preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_owner_id_users_id_fk": { + "name": "projects_owner_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "projects_team_id_teams_id_fk": { + "name": "projects_team_id_teams_id_fk", + "tableFrom": "projects", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_credentials": { + "name": "provider_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_type": { + "name": "credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "provider_credentials_user_provider_idx": { + "name": "provider_credentials_user_provider_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_credentials_user_id_idx": { + "name": "provider_credentials_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_credentials_user_id_users_id_fk": { + "name": "provider_credentials_user_id_users_id_fk", + "tableFrom": "provider_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routing_rules": { + "name": "routing_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routing_rules_scope_priority_idx": { + "name": "routing_rules_scope_priority_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_user_id_idx": { + "name": "routing_rules_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_enabled_idx": { + "name": "routing_rules_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routing_rules_user_id_users_id_fk": { + "name": "routing_rules_user_id_users_id_fk", + "tableFrom": "routing_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "installed_by": { + "name": "installed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skills_enabled_idx": { + "name": "skills_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skills_installed_by_users_id_fk": { + "name": "skills_installed_by_users_id_fk", + "tableFrom": "skills", + "tableTo": "users", + "columnsFrom": [ + "installed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "skills_name_unique": { + "name": "skills_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.summarization_jobs": { + "name": "summarization_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "logs_processed": { + "name": "logs_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "insights_created": { + "name": "insights_created", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "summarization_jobs_status_idx": { + "name": "summarization_jobs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee": { + "name": "assignee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_project_id_idx": { + "name": "tasks_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_mission_id_idx": { + "name": "tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_project_id_projects_id_fk": { + "name": "tasks_project_id_projects_id_fk", + "tableFrom": "tasks", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_mission_id_missions_id_fk": { + "name": "tasks_mission_id_missions_id_fk", + "tableFrom": "tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_members_team_user_idx": { + "name": "team_members_team_user_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_invited_by_users_id_fk": { + "name": "team_members_invited_by_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "teams_manager_id_users_id_fk": { + "name": "teams_manager_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_slug_unique": { + "name": "teams_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_checkpoints": { + "name": "interaction_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "compaction_epoch": { + "name": "compaction_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_checkpoints_session_idempotency_idx": { + "name": "interaction_checkpoints_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_checkpoints_session_epoch_idx": { + "name": "interaction_checkpoints_session_epoch_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "compaction_epoch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_checkpoints_session_id_interaction_sessions_id_fk": { + "name": "interaction_checkpoints_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_checkpoints", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_handoffs": { + "name": "interaction_handoffs", + "schema": "", + "columns": { + "handoff_id": { + "name": "handoff_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_handoff_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_handoffs_session_status_idx": { + "name": "interaction_handoffs_session_status_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_handoffs_session_id_interaction_sessions_id_fk": { + "name": "interaction_handoffs_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_handoffs", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_inbox": { + "name": "interaction_inbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_inbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_inbox_session_idempotency_idx": { + "name": "interaction_inbox_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_inbox_session_status_created_idx": { + "name": "interaction_inbox_session_status_created_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_inbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_inbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_inbox", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_outbox": { + "name": "interaction_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_outbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_outbox_session_idempotency_idx": { + "name": "interaction_outbox_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_outbox_session_status_created_idx": { + "name": "interaction_outbox_session_status_created_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_outbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_outbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_outbox", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_sessions": { + "name": "interaction_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime_session_id": { + "name": "runtime_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "interaction_sessions_owner_id_users_id_fk": { + "name": "interaction_sessions_owner_id_users_id_fk", + "tableFrom": "interaction_sessions", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tickets": { + "name": "tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tickets_status_idx": { + "name": "tickets_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.backlog_status": { + "name": "backlog_status", + "schema": "public", + "values": [ + "ready", + "claimed", + "blocked", + "done" + ] + }, + "public.grant_status": { + "name": "grant_status", + "schema": "public", + "values": [ + "pending", + "active", + "revoked", + "expired" + ] + }, + "public.peer_state": { + "name": "peer_state", + "schema": "public", + "values": [ + "pending", + "active", + "suspended", + "revoked" + ] + }, + "public.interaction_handoff_status": { + "name": "interaction_handoff_status", + "schema": "public", + "values": [ + "pending", + "accepted" + ] + }, + "public.interaction_inbox_status": { + "name": "interaction_inbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "processed" + ] + }, + "public.interaction_outbox_status": { + "name": "interaction_outbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "delivered" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/0015_snapshot.json b/packages/db/drizzle/meta/0015_snapshot.json new file mode 100644 index 0000000..ccb49fb --- /dev/null +++ b/packages/db/drizzle/meta/0015_snapshot.json @@ -0,0 +1,4244 @@ +{ + "id": "1a0a53b1-2dd8-4ea9-8d59-3e92ccca6c6a", + "prevId": "48124a40-a224-4d1c-ab8f-b3dab554b1ac", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_provider_account_idx": { + "name": "accounts_provider_account_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_user_id_idx": { + "name": "accounts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_tokens": { + "name": "admin_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_tokens_user_id_idx": { + "name": "admin_tokens_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_tokens_hash_idx": { + "name": "admin_tokens_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_tokens_user_id_users_id_fk": { + "name": "admin_tokens_user_id_users_id_fk", + "tableFrom": "admin_tokens", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_logs": { + "name": "agent_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'hot'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "summarized_at": { + "name": "summarized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_logs_session_tier_idx": { + "name": "agent_logs_session_tier_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_user_id_idx": { + "name": "agent_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_logs_tier_created_at_idx": { + "name": "agent_logs_tier_created_at_idx", + "columns": [ + { + "expression": "tier", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_logs_user_id_users_id_fk": { + "name": "agent_logs_user_id_users_id_fk", + "tableFrom": "agent_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_project_id_idx": { + "name": "agents_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_owner_id_idx": { + "name": "agents_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_is_system_idx": { + "name": "agents_is_system_idx", + "columns": [ + { + "expression": "is_system", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_project_id_projects_id_fk": { + "name": "agents_project_id_projects_id_fk", + "tableFrom": "agents", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "agents_owner_id_users_id_fk": { + "name": "agents_owner_id_users_id_fk", + "tableFrom": "agents", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.appreciations": { + "name": "appreciations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "from_user": { + "name": "from_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_user": { + "name": "to_user", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backlog": { + "name": "backlog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "backlog_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "depends_on": { + "name": "depends_on", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "claim_owner": { + "name": "claim_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_ttl_seconds": { + "name": "claim_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptance": { + "name": "acceptance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "backlog_status_priority_idx": { + "name": "backlog_status_priority_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backlog_status_claimed_at_idx": { + "name": "backlog_status_claimed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backlog_idempotency_key_idx": { + "name": "backlog_idempotency_key_idx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "conversations_user_archived_idx": { + "name": "conversations_user_archived_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_project_id_idx": { + "name": "conversations_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "conversations_agent_id_idx": { + "name": "conversations_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "conversations_user_id_users_id_fk": { + "name": "conversations_user_id_users_id_fk", + "tableFrom": "conversations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conversations_project_id_projects_id_fk": { + "name": "conversations_project_id_projects_id_fk", + "tableFrom": "conversations", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "conversations_agent_id_agents_id_fk": { + "name": "conversations_agent_id_agents_id_fk", + "tableFrom": "conversations", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "events_type_idx": { + "name": "events_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_date_idx": { + "name": "events_date_idx", + "columns": [ + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_audit_log": { + "name": "federation_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "verb": { + "name": "verb", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "denied_reason": { + "name": "denied_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "query_hash": { + "name": "query_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bytes_out": { + "name": "bytes_out", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_audit_log_peer_created_at_idx": { + "name": "federation_audit_log_peer_created_at_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_subject_created_at_idx": { + "name": "federation_audit_log_subject_created_at_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_audit_log_created_at_idx": { + "name": "federation_audit_log_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_audit_log_peer_id_federation_peers_id_fk": { + "name": "federation_audit_log_peer_id_federation_peers_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_subject_user_id_users_id_fk": { + "name": "federation_audit_log_subject_user_id_users_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "federation_audit_log_grant_id_federation_grants_id_fk": { + "name": "federation_audit_log_grant_id_federation_grants_id_fk", + "tableFrom": "federation_audit_log", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_enrollment_tokens": { + "name": "federation_enrollment_tokens", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "federation_enrollment_tokens_grant_id_federation_grants_id_fk": { + "name": "federation_enrollment_tokens_grant_id_federation_grants_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_enrollment_tokens_peer_id_federation_peers_id_fk": { + "name": "federation_enrollment_tokens_peer_id_federation_peers_id_fk", + "tableFrom": "federation_enrollment_tokens", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_grants": { + "name": "federation_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "peer_id": { + "name": "peer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "grant_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_reason": { + "name": "revoked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_grants_subject_status_idx": { + "name": "federation_grants_subject_status_idx", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_grants_peer_status_idx": { + "name": "federation_grants_peer_status_idx", + "columns": [ + { + "expression": "peer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "federation_grants_subject_user_id_users_id_fk": { + "name": "federation_grants_subject_user_id_users_id_fk", + "tableFrom": "federation_grants", + "tableTo": "users", + "columnsFrom": [ + "subject_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "federation_grants_peer_id_federation_peers_id_fk": { + "name": "federation_grants_peer_id_federation_peers_id_fk", + "tableFrom": "federation_grants", + "tableTo": "federation_peers", + "columnsFrom": [ + "peer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.federation_peers": { + "name": "federation_peers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "common_name": { + "name": "common_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_pem": { + "name": "cert_pem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_serial": { + "name": "cert_serial", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cert_not_after": { + "name": "cert_not_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "client_key_pem": { + "name": "client_key_pem", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "peer_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "endpoint_url": { + "name": "endpoint_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "federation_peers_cert_serial_idx": { + "name": "federation_peers_cert_serial_idx", + "columns": [ + { + "expression": "cert_serial", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "federation_peers_state_idx": { + "name": "federation_peers_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "federation_peers_common_name_unique": { + "name": "federation_peers_common_name_unique", + "nullsNotDistinct": false, + "columns": [ + "common_name" + ] + }, + "federation_peers_cert_serial_unique": { + "name": "federation_peers_cert_serial_unique", + "nullsNotDistinct": false, + "columns": [ + "cert_serial" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.insights": { + "name": "insights", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'agent'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "relevance_score": { + "name": "relevance_score", + "type": "real", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decayed_at": { + "name": "decayed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "insights_user_id_idx": { + "name": "insights_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_category_idx": { + "name": "insights_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "insights_relevance_idx": { + "name": "insights_relevance_idx", + "columns": [ + { + "expression": "relevance_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "insights_user_id_users_id_fk": { + "name": "insights_user_id_users_id_fk", + "tableFrom": "insights", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_checkpoints": { + "name": "interaction_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_digest": { + "name": "content_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "compaction_epoch": { + "name": "compaction_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_checkpoints_session_idempotency_idx": { + "name": "interaction_checkpoints_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_checkpoints_session_epoch_idx": { + "name": "interaction_checkpoints_session_epoch_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "compaction_epoch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_checkpoints_session_id_interaction_sessions_id_fk": { + "name": "interaction_checkpoints_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_checkpoints", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_handoffs": { + "name": "interaction_handoffs", + "schema": "", + "columns": { + "handoff_id": { + "name": "handoff_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_handoff_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_handoffs_session_status_idx": { + "name": "interaction_handoffs_session_status_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_handoffs_session_id_interaction_sessions_id_fk": { + "name": "interaction_handoffs_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_handoffs", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_inbox": { + "name": "interaction_inbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_digest": { + "name": "content_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_inbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_inbox_session_idempotency_idx": { + "name": "interaction_inbox_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_inbox_session_status_created_idx": { + "name": "interaction_inbox_session_status_created_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_inbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_inbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_inbox", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_outbox": { + "name": "interaction_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_id": { + "name": "correlation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_digest": { + "name": "content_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "interaction_outbox_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "interaction_outbox_session_idempotency_idx": { + "name": "interaction_outbox_session_idempotency_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "interaction_outbox_session_status_created_idx": { + "name": "interaction_outbox_session_status_created_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "interaction_outbox_session_id_interaction_sessions_id_fk": { + "name": "interaction_outbox_session_id_interaction_sessions_id_fk", + "tableFrom": "interaction_outbox", + "tableTo": "interaction_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.interaction_sessions": { + "name": "interaction_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime_session_id": { + "name": "runtime_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "interaction_sessions_owner_id_users_id_fk": { + "name": "interaction_sessions_owner_id_users_id_fk", + "tableFrom": "interaction_sessions", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "messages_conversation_id_idx": { + "name": "messages_conversation_id_idx", + "columns": [ + { + "expression": "conversation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "messages_conversation_id_conversations_id_fk": { + "name": "messages_conversation_id_conversations_id_fk", + "tableFrom": "messages", + "tableTo": "conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mission_tasks": { + "name": "mission_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr": { + "name": "pr", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mission_tasks_mission_id_idx": { + "name": "mission_tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_task_id_idx": { + "name": "mission_tasks_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_user_id_idx": { + "name": "mission_tasks_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_tasks_status_idx": { + "name": "mission_tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mission_tasks_mission_id_missions_id_fk": { + "name": "mission_tasks_mission_id_missions_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mission_tasks_task_id_tasks_id_fk": { + "name": "mission_tasks_task_id_tasks_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mission_tasks_user_id_users_id_fk": { + "name": "mission_tasks_user_id_users_id_fk", + "tableFrom": "mission_tasks", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions": { + "name": "missions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planning'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "milestones": { + "name": "milestones", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "missions_project_id_idx": { + "name": "missions_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_user_id_idx": { + "name": "missions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_project_id_projects_id_fk": { + "name": "missions_project_id_projects_id_fk", + "tableFrom": "missions", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_user_id_users_id_fk": { + "name": "missions_user_id_users_id_fk", + "tableFrom": "missions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preferences": { + "name": "preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mutable": { + "name": "mutable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "preferences_user_id_idx": { + "name": "preferences_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "preferences_user_key_idx": { + "name": "preferences_user_key_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "preferences_user_id_users_id_fk": { + "name": "preferences_user_id_users_id_fk", + "tableFrom": "preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_owner_id_users_id_fk": { + "name": "projects_owner_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "projects_team_id_teams_id_fk": { + "name": "projects_team_id_teams_id_fk", + "tableFrom": "projects", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.provider_credentials": { + "name": "provider_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_type": { + "name": "credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "provider_credentials_user_provider_idx": { + "name": "provider_credentials_user_provider_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_credentials_user_id_idx": { + "name": "provider_credentials_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_credentials_user_id_users_id_fk": { + "name": "provider_credentials_user_id_users_id_fk", + "tableFrom": "provider_credentials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routing_rules": { + "name": "routing_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routing_rules_scope_priority_idx": { + "name": "routing_rules_scope_priority_idx", + "columns": [ + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_user_id_idx": { + "name": "routing_rules_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routing_rules_enabled_idx": { + "name": "routing_rules_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routing_rules_user_id_users_id_fk": { + "name": "routing_rules_user_id_users_id_fk", + "tableFrom": "routing_rules", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skills": { + "name": "skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "installed_by": { + "name": "installed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skills_enabled_idx": { + "name": "skills_enabled_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skills_installed_by_users_id_fk": { + "name": "skills_installed_by_users_id_fk", + "tableFrom": "skills", + "tableTo": "users", + "columnsFrom": [ + "installed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "skills_name_unique": { + "name": "skills_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.summarization_jobs": { + "name": "summarization_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "logs_processed": { + "name": "logs_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "insights_created": { + "name": "insights_created", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "summarization_jobs_status_idx": { + "name": "summarization_jobs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mission_id": { + "name": "mission_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee": { + "name": "assignee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_project_id_idx": { + "name": "tasks_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_mission_id_idx": { + "name": "tasks_mission_id_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_project_id_projects_id_fk": { + "name": "tasks_project_id_projects_id_fk", + "tableFrom": "tasks", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_mission_id_missions_id_fk": { + "name": "tasks_mission_id_missions_id_fk", + "tableFrom": "tasks", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_members_team_user_idx": { + "name": "team_members_team_user_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_invited_by_users_id_fk": { + "name": "team_members_invited_by_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manager_id": { + "name": "manager_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_owner_id_users_id_fk": { + "name": "teams_owner_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "teams_manager_id_users_id_fk": { + "name": "teams_manager_id_users_id_fk", + "tableFrom": "teams", + "tableTo": "users", + "columnsFrom": [ + "manager_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_slug_unique": { + "name": "teams_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tickets": { + "name": "tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tickets_status_idx": { + "name": "tickets_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.backlog_status": { + "name": "backlog_status", + "schema": "public", + "values": [ + "ready", + "claimed", + "blocked", + "done" + ] + }, + "public.grant_status": { + "name": "grant_status", + "schema": "public", + "values": [ + "pending", + "active", + "revoked", + "expired" + ] + }, + "public.interaction_handoff_status": { + "name": "interaction_handoff_status", + "schema": "public", + "values": [ + "pending", + "accepted" + ] + }, + "public.interaction_inbox_status": { + "name": "interaction_inbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "processed" + ] + }, + "public.interaction_outbox_status": { + "name": "interaction_outbox_status", + "schema": "public", + "values": [ + "pending", + "processing", + "delivered" + ] + }, + "public.peer_state": { + "name": "peer_state", + "schema": "public", + "values": [ + "pending", + "active", + "suspended", + "revoked" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index fb09f9d..dcdb944 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -85,6 +85,34 @@ "when": 1782310438919, "tag": "0011_bitter_gateway", "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1783911983447, + "tag": "0012_interaction_durable_state", + "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1783913232578, + "tag": "0013_interaction_checkpoint_history", + "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1783913398006, + "tag": "0014_interaction_outbox_channel_scope", + "breakpoints": true + }, + { + "idx": 15, + "version": "7", + "when": 1783942610000, + "tag": "0015_interaction_checkpoint_payload_digest", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 7f343ce..76809d2 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -487,6 +487,120 @@ export const agentLogs = pgTable( ], ); +// ─── Tess durable session state ───────────────────────────────────────────── +// PostgreSQL is canonical for restart-safe Tess session recovery. The state +// machine lives in @mosaicstack/agent; these records are its durable adapter. + +export const interactionInboxStatusEnum = pgEnum('interaction_inbox_status', [ + 'pending', + 'processing', + 'processed', +]); +export const interactionOutboxStatusEnum = pgEnum('interaction_outbox_status', [ + 'pending', + 'processing', + 'delivered', +]); +export const interactionHandoffStatusEnum = pgEnum('interaction_handoff_status', [ + 'pending', + 'accepted', +]); + +export const interactionSessions = pgTable('interaction_sessions', { + id: text('id').primaryKey(), + agentName: text('agent_name').notNull(), + tenantId: text('tenant_id').notNull(), + ownerId: text('owner_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + providerId: text('provider_id').notNull(), + runtimeSessionId: text('runtime_session_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export const interactionInbox = pgTable( + 'interaction_inbox', + { + id: uuid('id').primaryKey().defaultRandom(), + sessionId: text('session_id') + .notNull() + .references(() => interactionSessions.id, { onDelete: 'cascade' }), + idempotencyKey: text('idempotency_key').notNull(), + correlationId: text('correlation_id').notNull(), + content: text('content').notNull(), + contentDigest: text('content_digest').notNull(), + status: interactionInboxStatusEnum('status').notNull().default('pending'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + uniqueIndex('interaction_inbox_session_idempotency_idx').on(t.sessionId, t.idempotencyKey), + index('interaction_inbox_session_status_created_idx').on(t.sessionId, t.status, t.createdAt), + ], +); + +export const interactionOutbox = pgTable( + 'interaction_outbox', + { + id: uuid('id').primaryKey().defaultRandom(), + sessionId: text('session_id') + .notNull() + .references(() => interactionSessions.id, { onDelete: 'cascade' }), + idempotencyKey: text('idempotency_key').notNull(), + correlationId: text('correlation_id').notNull(), + channelId: text('channel_id').notNull(), + kind: text('kind').notNull(), + content: text('content').notNull(), + contentDigest: text('content_digest').notNull(), + status: interactionOutboxStatusEnum('status').notNull().default('pending'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + uniqueIndex('interaction_outbox_session_idempotency_idx').on(t.sessionId, t.idempotencyKey), + index('interaction_outbox_session_status_created_idx').on(t.sessionId, t.status, t.createdAt), + ], +); + +export const interactionCheckpoints = pgTable( + 'interaction_checkpoints', + { + id: uuid('id').primaryKey().defaultRandom(), + sessionId: text('session_id') + .notNull() + .references(() => interactionSessions.id, { onDelete: 'cascade' }), + checkpointId: text('checkpoint_id').notNull(), + contentDigest: text('content_digest').notNull(), + cursor: text('cursor').notNull(), + summary: text('summary').notNull(), + compactionEpoch: integer('compaction_epoch').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [ + uniqueIndex('interaction_checkpoints_session_idempotency_idx').on(t.sessionId, t.checkpointId), + index('interaction_checkpoints_session_epoch_idx').on(t.sessionId, t.compactionEpoch), + ], +); + +export const interactionHandoffs = pgTable( + 'interaction_handoffs', + { + handoffId: text('handoff_id').primaryKey(), + sessionId: text('session_id') + .notNull() + .references(() => interactionSessions.id, { onDelete: 'cascade' }), + destination: text('destination').notNull(), + correlationId: text('correlation_id').notNull(), + checkpointId: text('checkpoint_id').notNull(), + status: interactionHandoffStatusEnum('status').notNull().default('pending'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), + }, + (t) => [index('interaction_handoffs_session_status_idx').on(t.sessionId, t.status)], +); + // ─── Skills ───────────────────────────────────────────────────────────────── export const skills = pgTable( diff --git a/packages/memory/src/adapters/keyword.test.ts b/packages/memory/src/adapters/keyword.test.ts index 2a0ac85..a56b800 100644 --- a/packages/memory/src/adapters/keyword.test.ts +++ b/packages/memory/src/adapters/keyword.test.ts @@ -274,6 +274,20 @@ describe('KeywordAdapter', () => { expect(results).toHaveLength(1); }); + it('should return all scoped insights for the explicit wildcard query', async () => { + await adapter.storeInsight({ + userId: 'u1', + content: 'A literal * marker is still ordinary content', + source: 'chat', + category: 'technical', + relevanceScore: 0.7, + }); + + const results = await adapter.searchInsights('u1', '*'); + expect(results).toHaveLength(4); + expect(results.every((result) => result.score === 1)).toBe(true); + }); + it('should return empty for empty query', async () => { const results = await adapter.searchInsights('u1', ' '); expect(results).toHaveLength(0); diff --git a/packages/memory/src/adapters/keyword.ts b/packages/memory/src/adapters/keyword.ts index 7575076..ea188e7 100644 --- a/packages/memory/src/adapters/keyword.ts +++ b/packages/memory/src/adapters/keyword.ts @@ -132,19 +132,23 @@ export class KeywordAdapter implements MemoryAdapter { opts?: { limit?: number; embedding?: number[] }, ): Promise { const limit = opts?.limit ?? 10; - const words = query - .toLowerCase() - .split(/\s+/) - .filter((w) => w.length > 0); + const normalizedQuery = query.trim(); + const matchAll = normalizedQuery === '*'; + const words = matchAll + ? [] + : normalizedQuery + .toLowerCase() + .split(/\s+/) + .filter((word) => word.length > 0); - if (words.length === 0) return []; + if (words.length === 0 && !matchAll) return []; const rows = await this.storage.find(INSIGHTS, { userId }); const scored: InsightSearchResult[] = []; for (const row of rows) { const content = row.content.toLowerCase(); - let score = 0; + let score = matchAll ? 1 : 0; for (const word of words) { if (content.includes(word)) score++; } diff --git a/packages/memory/src/index.ts b/packages/memory/src/index.ts index 97f734d..243a027 100644 --- a/packages/memory/src/index.ts +++ b/packages/memory/src/index.ts @@ -22,6 +22,13 @@ export type { InsightSearchResult, } from './types.js'; export { createMemoryAdapter, registerMemoryAdapter } from './factory.js'; +export { + createOperatorMemoryPlugin, + type OperatorMemoryPlugin, + type OperatorMemoryScope, + type OperatorMemoryConfig, + type OperatorMemoryResult, +} from './operator-memory-plugin.js'; export { PgVectorAdapter } from './adapters/pgvector.js'; export { KeywordAdapter } from './adapters/keyword.js'; diff --git a/packages/memory/src/operator-memory-plugin.test.ts b/packages/memory/src/operator-memory-plugin.test.ts new file mode 100644 index 0000000..26649db --- /dev/null +++ b/packages/memory/src/operator-memory-plugin.test.ts @@ -0,0 +1,147 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createOperatorMemoryPlugin } from './operator-memory-plugin.js'; +import type { Insight, InsightSearchResult, NewInsight } from './types.js'; + +function adapter() { + return { + name: 'test', + embedder: null, + storeInsight: vi.fn( + async (value: NewInsight): Promise => ({ + ...value, + id: '1', + createdAt: new Date(), + }), + ), + searchInsights: vi.fn(async (): Promise => []), + getInsight: vi.fn(), + deleteInsight: vi.fn(), + getPreference: vi.fn(), + setPreference: vi.fn(), + deletePreference: vi.fn(), + listPreferences: vi.fn(), + close: vi.fn(), + }; +} + +describe('OperatorMemoryPlugin', () => { + it('isolates configured namespace storage across server-derived tenant, owner, and session scopes', async () => { + const memory = adapter(); + const plugin = createOperatorMemoryPlugin({ + adapter: memory, + instanceId: 'Nova', + namespace: 'operator-memory', + redact: (value) => value, + }); + await plugin.capture( + { tenantId: 'tenant-a', ownerId: 'owner-a', sessionId: 'session-a' }, + { content: 'one', source: 'test', category: 'note' }, + ); + await plugin.capture( + { tenantId: 'tenant-b', ownerId: 'owner-a', sessionId: 'session-a' }, + { content: 'two', source: 'test', category: 'note' }, + ); + await plugin.capture( + { tenantId: 'tenant-a', ownerId: 'owner-a', sessionId: 'session-b' }, + { content: 'three', source: 'test', category: 'note' }, + ); + + expect( + memory.storeInsight.mock.calls.map((call: unknown[]) => (call[0] as NewInsight).userId), + ).toEqual([ + '["operator-memory","tenant-a","owner-a","session-a"]', + '["operator-memory","tenant-b","owner-a","session-a"]', + '["operator-memory","tenant-a","owner-a","session-b"]', + ]); + }); + + it('rejects an incomplete runtime scope before it can produce a shared storage key', async () => { + const memory = adapter(); + const plugin = createOperatorMemoryPlugin({ + adapter: memory, + instanceId: 'Nova', + namespace: 'operator-memory', + redact: (value) => value, + }); + + await expect( + plugin.capture( + { tenantId: 'tenant-a', ownerId: 'owner-a', sessionId: ' ' }, + { content: 'note', source: 'test', category: 'note' }, + ), + ).rejects.toThrow('Operator memory session ID is required'); + expect(memory.storeInsight).not.toHaveBeenCalled(); + }); + + it('uses a differently named configured instance in retrieval provenance', async () => { + const memory = adapter(); + memory.searchInsights.mockResolvedValue([ + { id: '1', content: 'x', score: 1, metadata: { source: 'project' } }, + ]); + const plugin = createOperatorMemoryPlugin({ + adapter: memory, + instanceId: 'Nova', + namespace: 'operator-memory', + redact: (value) => value, + }); + + const results = await plugin.search({ tenantId: 't', ownerId: 'o', sessionId: 's' }, 'x'); + + expect(results[0]?.provenance).toEqual({ + instanceId: 'Nova', + namespace: 'operator-memory', + source: 'project', + }); + }); + + it('orders startup context with project and flat-file truth before retrieved material', async () => { + const memory = adapter(); + memory.searchInsights.mockResolvedValue([ + { id: 'retrieval', content: 'retrieval', score: 1 }, + { id: 'flat-file', content: 'flat-file', score: 1, metadata: { source: 'flat-file' } }, + { id: 'project', content: 'project', score: 1, metadata: { source: 'project' } }, + ]); + const plugin = createOperatorMemoryPlugin({ + adapter: memory, + instanceId: 'Nova', + namespace: 'operator-memory', + maxStartupContext: 2, + redact: (value) => value, + }); + + const context = await plugin.startupContext({ + tenantId: 'tenant-a', + ownerId: 'owner-a', + sessionId: 'session-a', + }); + + expect(context.map((result) => result.id)).toEqual(['project', 'flat-file']); + expect(memory.searchInsights).toHaveBeenCalledWith(expect.any(String), '*', { limit: 64 }); + }); + + it('redacts content before adapter persistence and records configured provenance metadata', async () => { + const memory = adapter(); + const plugin = createOperatorMemoryPlugin({ + adapter: memory, + instanceId: 'Nova', + namespace: 'operator-memory', + redact: (value) => value.replace('secret', '[REDACTED]'), + }); + + await plugin.capture( + { tenantId: 'tenant-a', ownerId: 'owner-a', sessionId: 'session-a' }, + { content: 'secret note', source: 'project', category: 'note' }, + ); + + expect(memory.storeInsight).toHaveBeenCalledWith( + expect.objectContaining({ + content: '[REDACTED] note', + metadata: { + instanceId: 'Nova', + namespace: 'operator-memory', + source: 'project', + }, + }), + ); + }); +}); diff --git a/packages/memory/src/operator-memory-plugin.ts b/packages/memory/src/operator-memory-plugin.ts new file mode 100644 index 0000000..698d904 --- /dev/null +++ b/packages/memory/src/operator-memory-plugin.ts @@ -0,0 +1,154 @@ +import type { Insight, InsightSearchResult, MemoryAdapter } from './types.js'; + +const STARTUP_CONTEXT_CANDIDATE_LIMIT = 64; + +/** Immutable server-derived boundary; callers never choose an adapter namespace. */ +export interface OperatorMemoryScope { + readonly tenantId: string; + readonly ownerId: string; + readonly sessionId: string; +} + +export interface OperatorMemoryConfig { + /** Adapter injection is deployment/lifecycle configuration, never caller input. */ + readonly adapter: MemoryAdapter; + /** Configured agent identity; it is metadata rather than a storage key default. */ + readonly instanceId: string; + /** Configured storage partition; callers cannot select a namespace. */ + readonly namespace: string; + readonly maxStartupContext?: number; + redact(content: string): string; +} + +export interface OperatorMemoryResult extends InsightSearchResult { + provenance: { instanceId: string; namespace: string; source: string }; +} + +export interface OperatorMemoryPlugin { + capture( + scope: OperatorMemoryScope, + input: { content: string; source: string; category: string }, + ): Promise; + search( + scope: OperatorMemoryScope, + query: string, + limit?: number, + ): Promise; + recent(scope: OperatorMemoryScope, limit?: number): Promise; + stats(scope: OperatorMemoryScope): Promise<{ namespace: string; resultCount: number }>; + startupContext(scope: OperatorMemoryScope): Promise; +} + +function scopedUserId(scope: OperatorMemoryScope, namespace: string): string { + const normalizedScope = normalizeScope(scope); + // JSON tuple encoding avoids delimiter collisions between independently scoped IDs. + return JSON.stringify([ + namespace, + normalizedScope.tenantId, + normalizedScope.ownerId, + normalizedScope.sessionId, + ]); +} + +function normalizeScope(scope: OperatorMemoryScope): OperatorMemoryScope { + if (typeof scope !== 'object' || scope === null) { + throw new Error('Operator memory scope is required'); + } + return Object.freeze({ + tenantId: requiredScopeId(scope.tenantId, 'tenant ID'), + ownerId: requiredScopeId(scope.ownerId, 'owner ID'), + sessionId: requiredScopeId(scope.sessionId, 'session ID'), + }); +} + +function requiredScopeId(value: unknown, field: string): string { + if (typeof value !== 'string' || value.trim().length === 0) { + throw new Error(`Operator memory ${field} is required`); + } + return value.trim(); +} + +function compareStartupContext(left: OperatorMemoryResult, right: OperatorMemoryResult): number { + return ( + startupSourcePriority(left.provenance.source) - startupSourcePriority(right.provenance.source) + ); +} + +function startupSourcePriority(source: string): number { + if (source === 'project') return 0; + if (source === 'flat-file') return 1; + return 2; +} + +function normalizeConfig(config: OperatorMemoryConfig): OperatorMemoryConfig { + const instanceId = config.instanceId.trim(); + const namespace = config.namespace.trim(); + const maxStartupContext = config.maxStartupContext ?? 8; + if (instanceId.length === 0 || namespace.length === 0) { + throw new Error('Operator memory instance ID and namespace must be configured'); + } + if (!Number.isSafeInteger(maxStartupContext) || maxStartupContext < 1) { + throw new Error('Operator memory startup context limit must be a positive integer'); + } + return Object.freeze({ ...config, instanceId, namespace, maxStartupContext }); +} + +/** Creates a leaf-package, replaceable memory adapter facade. */ +export function createOperatorMemoryPlugin(config: OperatorMemoryConfig): OperatorMemoryPlugin { + const pluginConfig = normalizeConfig(config); + const mapResult = (result: InsightSearchResult): OperatorMemoryResult => ({ + ...result, + provenance: { + instanceId: pluginConfig.instanceId, + namespace: pluginConfig.namespace, + source: String(result.metadata?.['source'] ?? 'retrieval'), + }, + }); + const search = async ( + scope: OperatorMemoryScope, + query: string, + limit = 10, + ): Promise => + ( + await pluginConfig.adapter.searchInsights( + scopedUserId(scope, pluginConfig.namespace), + query, + { + limit, + }, + ) + ).map(mapResult); + return { + async capture(scope, input) { + return pluginConfig.adapter.storeInsight({ + userId: scopedUserId(scope, pluginConfig.namespace), + content: pluginConfig.redact(input.content), + source: input.source, + category: input.category, + relevanceScore: 1, + metadata: { + namespace: pluginConfig.namespace, + instanceId: pluginConfig.instanceId, + source: input.source, + }, + }); + }, + search, + async recent(scope, limit = 10) { + return search(scope, '*', limit); + }, + async stats(scope) { + return { + namespace: pluginConfig.namespace, + resultCount: (await search(scope, '*', 100)).length, + }; + }, + async startupContext(scope) { + const maxStartupContext = pluginConfig.maxStartupContext ?? 8; + // Prioritize authoritative sources within a bounded candidate window. + const candidateLimit = Math.max(maxStartupContext, STARTUP_CONTEXT_CANDIDATE_LIMIT); + const context = await search(scope, '*', candidateLimit); + return [...context].sort(compareStartupContext).slice(0, maxStartupContext); + }, + }; +} diff --git a/packages/memory/src/types.ts b/packages/memory/src/types.ts index 47f9bf0..22aeb41 100644 --- a/packages/memory/src/types.ts +++ b/packages/memory/src/types.ts @@ -49,6 +49,10 @@ export interface MemoryAdapter { // Insights storeInsight(insight: NewInsight): Promise; getInsight(id: string): Promise; + /** + * Searches within one scoped user ID. The reserved `*` query returns scoped + * recent/all results rather than performing backend-specific wildcard parsing. + */ searchInsights( userId: string, query: string, diff --git a/packages/mosaic/framework/fleet/README.md b/packages/mosaic/framework/fleet/README.md index b1480fd..36cfb1e 100644 --- a/packages/mosaic/framework/fleet/README.md +++ b/packages/mosaic/framework/fleet/README.md @@ -15,6 +15,21 @@ default tmux server. - `examples/minimal.yaml` starts one local canary slot. - `examples/local-canary.yaml` starts a small generic dogfood fleet. +- `examples/operator-interaction.yaml` is an example Pi operator-interaction + service; replace its example agent name before provisioning. + +## Operator interaction service + +`services/operator-interaction.yaml` pins the Pi runtime, GPT-5.6 Sol model, +high reasoning, and the `operator-interaction` tool policy. The agent identity +is provisioning data: choose a roster name, generate its per-agent environment +file, then start the matching generic systemd instance. The service fails before +launch if the configured identity does not match the instance or any pinned +policy field drifts. + +The installed `tools/fleet/print-interaction-effective-policy.sh` prints only +the resolved name, runtime, model, reasoning, and tool policy. It never reads +or prints credential variables. Initialize a roster: diff --git a/packages/mosaic/framework/fleet/examples/operator-interaction.yaml b/packages/mosaic/framework/fleet/examples/operator-interaction.yaml new file mode 100644 index 0000000..1f9a4f9 --- /dev/null +++ b/packages/mosaic/framework/fleet/examples/operator-interaction.yaml @@ -0,0 +1,19 @@ +# Example instance only. Replace `Tess` with the chosen provisioned identity. +version: 1 +transport: tmux +tmux: + socket_name: mosaic-fleet + holder_session: _holder +defaults: + working_directory: ~/src +runtimes: + pi: + reset_command: /new +agents: + - name: Tess + runtime: pi + class: operator-interaction + model_hint: openai/gpt-5.6-sol + reasoning_level: high + tool_policy: operator-interaction + persistent_persona: true diff --git a/packages/mosaic/framework/fleet/roles/operator-interaction.md b/packages/mosaic/framework/fleet/roles/operator-interaction.md new file mode 100644 index 0000000..48f2490 --- /dev/null +++ b/packages/mosaic/framework/fleet/roles/operator-interaction.md @@ -0,0 +1,11 @@ +# Operator Interaction — fleet role definition + +The **operator-interaction** role is the authorized human interaction plane for +Mosaic. It presents runtime and fleet state, mediates approved actions, and +hands coding or general orchestration work to Mos. + +## Boundaries + +- It does not claim Mos-owned coding or general orchestration work. +- It exposes only the configured, observable tool policy. +- It does not receive or surface credentials in its effective policy. diff --git a/packages/mosaic/framework/fleet/roster.schema.json b/packages/mosaic/framework/fleet/roster.schema.json index 4327075..85b862c 100644 --- a/packages/mosaic/framework/fleet/roster.schema.json +++ b/packages/mosaic/framework/fleet/roster.schema.json @@ -105,6 +105,18 @@ "modelHint": { "type": "string" }, + "reasoning_level": { + "type": "string" + }, + "reasoningLevel": { + "type": "string" + }, + "tool_policy": { + "type": "string" + }, + "toolPolicy": { + "type": "string" + }, "persistent_persona": { "oneOf": [{ "type": "boolean" }, { "type": "string" }] }, diff --git a/packages/mosaic/framework/fleet/services/operator-interaction.yaml b/packages/mosaic/framework/fleet/services/operator-interaction.yaml new file mode 100644 index 0000000..1d016fd --- /dev/null +++ b/packages/mosaic/framework/fleet/services/operator-interaction.yaml @@ -0,0 +1,5 @@ +# Generic service policy. Provisioning supplies the agent name as data. +runtime: pi +model: openai/gpt-5.6-sol +reasoning: high +tool_policy: operator-interaction diff --git a/packages/mosaic/framework/systemd/user/README.md b/packages/mosaic/framework/systemd/user/README.md index f1e0b8a..1811ead 100644 --- a/packages/mosaic/framework/systemd/user/README.md +++ b/packages/mosaic/framework/systemd/user/README.md @@ -12,6 +12,8 @@ exact-match session. - `mosaic-tmux-holder.service` — user-mode holder that owns the named tmux server. - `mosaic-agent@.service` — user-mode template for one reusable agent session. +- `mosaic-interaction-agent@.service` — generic Pi operator-interaction template + that fails fast when its pinned runtime policy is incomplete or changed. - `test-fleet-units.sh` — validates unit syntax and required relationships. The agent template calls: @@ -45,12 +47,22 @@ MOSAIC_AGENT_WORKDIR=$HOME/src/your-project ```bash mkdir -p ~/.config/systemd/user ~/.config/mosaic/tools/fleet ~/.config/mosaic/fleet/agents cp packages/mosaic/framework/systemd/user/mosaic-*.service ~/.config/systemd/user/ -cp packages/mosaic/framework/tools/fleet/start-agent-session.sh ~/.config/mosaic/tools/fleet/ -chmod +x ~/.config/mosaic/tools/fleet/start-agent-session.sh +cp packages/mosaic/framework/tools/fleet/*.sh ~/.config/mosaic/tools/fleet/ +chmod +x ~/.config/mosaic/tools/fleet/*.sh systemctl --user daemon-reload systemctl --user start mosaic-tmux-holder.service systemctl --user start mosaic-agent@canary.service tmux -L mosaic-fleet ls + +# For an operator-interaction service, the roster/env identity selects the +# generic unit instance; no service source is renamed for an instance. +systemctl --user start mosaic-interaction-agent@.service +MOSAIC_AGENT_NAME= \ +MOSAIC_AGENT_RUNTIME=pi \ +MOSAIC_AGENT_MODEL=openai/gpt-5.6-sol \ +MOSAIC_AGENT_REASONING=high \ +MOSAIC_AGENT_TOOL_POLICY=operator-interaction \ + ~/.config/mosaic/tools/fleet/print-interaction-effective-policy.sh ``` Do not use `tmux kill-server` without `-L mosaic-fleet`; this pattern is meant diff --git a/packages/mosaic/framework/systemd/user/mosaic-interaction-agent@.service b/packages/mosaic/framework/systemd/user/mosaic-interaction-agent@.service new file mode 100644 index 0000000..346dbd8 --- /dev/null +++ b/packages/mosaic/framework/systemd/user/mosaic-interaction-agent@.service @@ -0,0 +1,17 @@ +[Unit] +Description=Mosaic operator interaction agent %i +Documentation=https://git.mosaicstack.dev/mosaicstack/stack +Requires=mosaic-tmux-holder.service +After=mosaic-tmux-holder.service +PartOf=mosaic-tmux-holder.service + +[Service] +Type=oneshot +RemainAfterExit=yes +Environment=MOSAIC_AGENT_NAME=%i +EnvironmentFile=%h/.config/mosaic/fleet/agents/%i.env +ExecStart=/bin/bash %h/.config/mosaic/tools/fleet/start-interaction-service.sh %i +ExecStop=-/bin/bash -lc 'if [ -n "${MOSAIC_TMUX_SOCKET:-}" ]; then tmux -L "$MOSAIC_TMUX_SOCKET" kill-session -t "=%i"; else tmux kill-session -t "=%i"; fi' + +[Install] +WantedBy=default.target diff --git a/packages/mosaic/framework/systemd/user/test-fleet-units.sh b/packages/mosaic/framework/systemd/user/test-fleet-units.sh index de5d7c2..041a325 100755 --- a/packages/mosaic/framework/systemd/user/test-fleet-units.sh +++ b/packages/mosaic/framework/systemd/user/test-fleet-units.sh @@ -4,6 +4,7 @@ set -euo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd) HOLDER="$SCRIPT_DIR/mosaic-tmux-holder.service" AGENT="$SCRIPT_DIR/mosaic-agent@.service" +INTERACTION="$SCRIPT_DIR/mosaic-interaction-agent@.service" fail() { echo "FAIL: $*" >&2 @@ -12,6 +13,7 @@ fail() { [ -f "$HOLDER" ] || fail "missing mosaic-tmux-holder.service" [ -f "$AGENT" ] || fail "missing mosaic-agent@.service" +[ -f "$INTERACTION" ] || fail "missing mosaic-interaction-agent@.service" grep -qF 'ExecStart=' "$HOLDER" || fail "holder has no ExecStart" grep -qF 'tmux -L' "$HOLDER" || fail "holder does not use named tmux socket" @@ -19,9 +21,12 @@ grep -qF '_holder' "$HOLDER" || fail "holder session is not explicit" grep -qF 'Requires=mosaic-tmux-holder.service' "$AGENT" || fail "agent does not require holder" grep -qF 'start-agent-session.sh' "$AGENT" || fail "agent unit does not call start-agent-session.sh" grep -qF 'kill-session -t "=%i"' "$AGENT" || fail "agent stop does not exact-match its session" +grep -qF 'Requires=mosaic-tmux-holder.service' "$INTERACTION" || fail "interaction service does not require holder" +grep -qF 'EnvironmentFile=%h/.config/mosaic/fleet/agents/%i.env' "$INTERACTION" || fail "interaction service does not require per-agent config" +grep -qF 'start-interaction-service.sh %i' "$INTERACTION" || fail "interaction service does not validate before startup" if command -v systemd-analyze >/dev/null 2>&1; then - systemd-analyze verify --user "$HOLDER" "$AGENT" >/tmp/mosaic-fleet-systemd-verify.log 2>&1 || { + systemd-analyze verify --user "$HOLDER" "$AGENT" "$INTERACTION" >/tmp/mosaic-fleet-systemd-verify.log 2>&1 || { cat /tmp/mosaic-fleet-systemd-verify.log >&2 fail "systemd-analyze verify failed" } diff --git a/packages/mosaic/framework/tools/fleet/print-interaction-effective-policy.sh b/packages/mosaic/framework/tools/fleet/print-interaction-effective-policy.sh new file mode 100755 index 0000000..9f31e0a --- /dev/null +++ b/packages/mosaic/framework/tools/fleet/print-interaction-effective-policy.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +AGENT_NAME=${MOSAIC_AGENT_NAME:-} +RUNTIME=${MOSAIC_AGENT_RUNTIME:-} +MODEL=${MOSAIC_AGENT_MODEL:-} +REASONING=${MOSAIC_AGENT_REASONING:-} +TOOL_POLICY=${MOSAIC_AGENT_TOOL_POLICY:-} + +[ -n "$AGENT_NAME" ] || { echo 'ERROR: MOSAIC_AGENT_NAME is required' >&2; exit 64; } +[[ "$AGENT_NAME" =~ ^[A-Za-z0-9_.-]+$ ]] || { echo 'ERROR: invalid agent name' >&2; exit 64; } +[ "$RUNTIME" = 'pi' ] || { echo 'ERROR: invalid runtime policy' >&2; exit 64; } +[ "$MODEL" = 'openai/gpt-5.6-sol' ] || { echo 'ERROR: invalid model policy' >&2; exit 64; } +[ "$REASONING" = 'high' ] || { echo 'ERROR: invalid reasoning policy' >&2; exit 64; } +[ "$TOOL_POLICY" = 'operator-interaction' ] || { echo 'ERROR: invalid tool policy' >&2; exit 64; } + +printf '{"agentName":"%s","runtime":"%s","model":"%s","reasoning":"%s","toolPolicy":"%s"}\n' \ + "$AGENT_NAME" "$RUNTIME" "$MODEL" "$REASONING" "$TOOL_POLICY" diff --git a/packages/mosaic/framework/tools/fleet/start-agent-session.sh b/packages/mosaic/framework/tools/fleet/start-agent-session.sh index 7bb9731..e5c9f2c 100755 --- a/packages/mosaic/framework/tools/fleet/start-agent-session.sh +++ b/packages/mosaic/framework/tools/fleet/start-agent-session.sh @@ -8,6 +8,7 @@ AGENT_NAME=${1:-${MOSAIC_AGENT_NAME:-}} MOSAIC_TMUX_SOCKET=${MOSAIC_TMUX_SOCKET:-} MOSAIC_AGENT_RUNTIME=${MOSAIC_AGENT_RUNTIME:-pi} MOSAIC_AGENT_MODEL=${MOSAIC_AGENT_MODEL:-} +MOSAIC_AGENT_REASONING=${MOSAIC_AGENT_REASONING:-} MOSAIC_AGENT_WORKDIR=${MOSAIC_AGENT_WORKDIR:-$HOME} MOSAIC_AGENT_COMMAND=${MOSAIC_AGENT_COMMAND:-} MOSAIC_HEARTBEAT_RUN_DIR=${MOSAIC_HEARTBEAT_RUN_DIR:-${MOSAIC_HOME:-$HOME/.config/mosaic}/fleet/run} @@ -18,6 +19,14 @@ if [ -z "$AGENT_NAME" ]; then exit 64 fi +case "$MOSAIC_AGENT_REASONING" in + ''|low|medium|high) ;; + *) + echo "ERROR: MOSAIC_AGENT_REASONING must be low, medium, or high" >&2 + exit 64 + ;; +esac + if ! command -v tmux >/dev/null 2>&1; then echo "ERROR: tmux is required" >&2 exit 69 @@ -41,7 +50,7 @@ fi if [ -z "$MOSAIC_AGENT_COMMAND" ]; then # Map the roster's per-agent model_hint to `--model` so workers launch on the # configured model (e.g. pi on openai-codex/gpt-5.5:high). Omitted when unset. - MOSAIC_AGENT_COMMAND="mosaic yolo $MOSAIC_AGENT_RUNTIME${MOSAIC_AGENT_MODEL:+ --model $MOSAIC_AGENT_MODEL}" + MOSAIC_AGENT_COMMAND="mosaic yolo $MOSAIC_AGENT_RUNTIME${MOSAIC_AGENT_MODEL:+ --model $MOSAIC_AGENT_MODEL}${MOSAIC_AGENT_REASONING:+ --thinking $MOSAIC_AGENT_REASONING}" fi # ── Derive a runtime-bin PATH prefix ───────────────────────────────────────── @@ -124,11 +133,12 @@ AGENT_NAME_Q=$(printf '%q' "$AGENT_NAME") # safe single bash token; an empty/unset class %q-quotes to '' and is a harmless # no-op downstream (readPersonaContractBlock returns '' for an empty class). AGENT_CLASS_Q=$(printf '%q' "${MOSAIC_AGENT_CLASS:-}") +AGENT_TOOL_POLICY_Q=$(printf '%q' "${MOSAIC_AGENT_TOOL_POLICY:-}") if [ -n "$MOSAIC_RUNTIME_BIN_PREFIX" ]; then - PANE_SHELL_SNIPPET="export MOSAIC_AGENT_NAME=${AGENT_NAME_Q}; export MOSAIC_AGENT_CLASS=${AGENT_CLASS_Q}; export PATH=\"${MOSAIC_RUNTIME_BIN_PREFIX}:\${PATH}\"; exec ${MOSAIC_AGENT_COMMAND}" + PANE_SHELL_SNIPPET="export MOSAIC_AGENT_NAME=${AGENT_NAME_Q}; export MOSAIC_AGENT_CLASS=${AGENT_CLASS_Q}; export MOSAIC_AGENT_TOOL_POLICY=${AGENT_TOOL_POLICY_Q}; export PATH=\"${MOSAIC_RUNTIME_BIN_PREFIX}:\${PATH}\"; exec ${MOSAIC_AGENT_COMMAND}" else - PANE_SHELL_SNIPPET="export MOSAIC_AGENT_NAME=${AGENT_NAME_Q}; export MOSAIC_AGENT_CLASS=${AGENT_CLASS_Q}; exec ${MOSAIC_AGENT_COMMAND}" + PANE_SHELL_SNIPPET="export MOSAIC_AGENT_NAME=${AGENT_NAME_Q}; export MOSAIC_AGENT_CLASS=${AGENT_CLASS_Q}; export MOSAIC_AGENT_TOOL_POLICY=${AGENT_TOOL_POLICY_Q}; exec ${MOSAIC_AGENT_COMMAND}" fi mkdir -p "$MOSAIC_AGENT_WORKDIR" diff --git a/packages/mosaic/framework/tools/fleet/start-interaction-service.sh b/packages/mosaic/framework/tools/fleet/start-interaction-service.sh new file mode 100755 index 0000000..4efc539 --- /dev/null +++ b/packages/mosaic/framework/tools/fleet/start-interaction-service.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +AGENT_NAME=${1:-} + +fail() { + echo "ERROR: $*" >&2 + exit 64 +} + +[ -n "$AGENT_NAME" ] || fail "agent name argument is required" +[[ "$AGENT_NAME" =~ ^[A-Za-z0-9_.-]+$ ]] || fail "agent name contains unsupported characters" +[ "${MOSAIC_AGENT_NAME:-}" = "$AGENT_NAME" ] || fail "configured agent name must exactly match the service instance" +[ "${MOSAIC_AGENT_RUNTIME:-}" = "pi" ] || fail "operator interaction service requires runtime pi" +[ "${MOSAIC_AGENT_MODEL:-}" = "openai/gpt-5.6-sol" ] || fail "operator interaction service requires the pinned model" +[ "${MOSAIC_AGENT_REASONING:-}" = "high" ] || fail "operator interaction service requires high reasoning" +[ "${MOSAIC_AGENT_TOOL_POLICY:-}" = "operator-interaction" ] || fail "operator interaction service requires the operator-interaction tool policy" + +exec "$(cd -- "$(dirname -- "$0")" && pwd)/start-agent-session.sh" "$AGENT_NAME" diff --git a/packages/mosaic/framework/tools/fleet/test-start-agent-session.sh b/packages/mosaic/framework/tools/fleet/test-start-agent-session.sh index 837ef93..0e83ecc 100755 --- a/packages/mosaic/framework/tools/fleet/test-start-agent-session.sh +++ b/packages/mosaic/framework/tools/fleet/test-start-agent-session.sh @@ -105,6 +105,7 @@ MOSAIC_TMUX_SOCKET="$SOCKET3" \ MOSAIC_AGENT_WORKDIR="$WORKDIR3" \ MOSAIC_AGENT_RUNTIME="pi" \ MOSAIC_AGENT_CLASS="code" \ +MOSAIC_AGENT_TOOL_POLICY="operator-interaction" \ MOSAIC_RUNTIME_BIN="$FAKE_RUNTIME_BIN" \ MOSAIC_AGENT_COMMAND="mosaic yolo pi --model openai-codex/gpt-5.5:high" \ MOSAIC_HEARTBEAT_RUN_DIR="$HB_RUN_DIR3" \ @@ -139,6 +140,8 @@ echo "$all_args" | grep -qF "export MOSAIC_AGENT_NAME=" || \ fail "pane command does not export MOSAIC_AGENT_NAME into the pane" echo "$all_args" | grep -qF "export MOSAIC_AGENT_CLASS=code" || \ fail "pane command does not export MOSAIC_AGENT_CLASS into the pane (persona would silently drop)" +echo "$all_args" | grep -qF "export MOSAIC_AGENT_TOOL_POLICY=operator-interaction" || \ + fail "pane command does not export MOSAIC_AGENT_TOOL_POLICY into the pane" # ── Test 4: when no extra runtime-bin dirs exist, exec still appears ─────────── TMUX_ARGS_FILE2=$(mktemp) diff --git a/packages/mosaic/src/cli.ts b/packages/mosaic/src/cli.ts index 4381b84..8fc5fad 100644 --- a/packages/mosaic/src/cli.ts +++ b/packages/mosaic/src/cli.ts @@ -12,6 +12,7 @@ import { registerQueueCommand } from '@mosaicstack/queue'; import { registerStorageCommand } from '@mosaicstack/storage'; import { registerTelemetryCommand } from './commands/telemetry.js'; import { registerAgentCommand } from './commands/agent.js'; +import { registerInteractionCommand } from './commands/interaction.js'; import { registerConfigCommand } from './commands/config.js'; import { registerFleetCommand } from './commands/fleet.js'; import { registerMissionCommand } from './commands/mission.js'; @@ -352,6 +353,10 @@ registerFederationCommand(program); registerAgentCommand(program); +// ─── interaction ─────────────────────────────────────────────────────── + +registerInteractionCommand(program); + // ─── fleet ───────────────────────────────────────────────────────────── registerFleetCommand(program); diff --git a/packages/mosaic/src/commands/compose-contract.spec.ts b/packages/mosaic/src/commands/compose-contract.spec.ts index bc021e4..6fcef51 100644 --- a/packages/mosaic/src/commands/compose-contract.spec.ts +++ b/packages/mosaic/src/commands/compose-contract.spec.ts @@ -165,6 +165,19 @@ describe('composeContract — overlay composer', () => { expect(composeContract('codex', fixture.home)).not.toContain('# pi runtime contract'); }); + it('injects the configured operator-interaction tool policy into the runtime contract', () => { + const previous = process.env['MOSAIC_AGENT_TOOL_POLICY']; + try { + process.env['MOSAIC_AGENT_TOOL_POLICY'] = 'operator-interaction'; + const out = composeContract('pi', fixture.home); + expect(out).toContain('# Fleet Tool Policy (operator-interaction)'); + expect(out).toContain('Denied by default: coding/general orchestration claims'); + } finally { + if (previous === undefined) delete process.env['MOSAIC_AGENT_TOOL_POLICY']; + else process.env['MOSAIC_AGENT_TOOL_POLICY'] = previous; + } + }); + // ── Persona contract injection (A3b) ────────────────────────────────────── // composeContract reads MOSAIC_AGENT_CLASS and injects the resolved persona // (override-aware). Save/restore the env so these tests don't leak state. diff --git a/packages/mosaic/src/commands/fleet.ts b/packages/mosaic/src/commands/fleet.ts index d287db5..293fc9b 100644 --- a/packages/mosaic/src/commands/fleet.ts +++ b/packages/mosaic/src/commands/fleet.ts @@ -87,6 +87,10 @@ interface RawFleetRoster { workingDirectory?: unknown; model_hint?: unknown; modelHint?: unknown; + reasoning_level?: unknown; + reasoningLevel?: unknown; + tool_policy?: unknown; + toolPolicy?: unknown; persistent_persona?: unknown; persistentPersona?: unknown; reset_between_tasks?: unknown; @@ -102,6 +106,8 @@ export interface FleetAgent { className: string; workingDirectory?: string; modelHint?: string; + reasoningLevel?: string; + toolPolicy?: string; persistentPersona?: boolean | string; resetBetweenTasks?: boolean; kickstartTemplate?: string; @@ -510,6 +516,12 @@ export function generateAgentEnv(roster: FleetRoster, agent: FleetAgent): string // the `mosaic yolo` launch so workers run on the roster's model (e.g. pi on // openai-codex/gpt-5.5:high). Empty when the agent declares no model_hint. `MOSAIC_AGENT_MODEL=${shellEnvValue(agent.modelHint ?? '')}`, + ...(agent.reasoningLevel !== undefined + ? [`MOSAIC_AGENT_REASONING=${shellEnvValue(agent.reasoningLevel)}`] + : []), + ...(agent.toolPolicy !== undefined + ? [`MOSAIC_AGENT_TOOL_POLICY=${shellEnvValue(agent.toolPolicy)}`] + : []), `MOSAIC_AGENT_WORKDIR=${shellEnvValue(expandHome(workingDirectory))}`, `MOSAIC_TMUX_SOCKET=${shellEnvValue(roster.tmux.socketName)}`, '', @@ -2316,13 +2328,35 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise await mkdir(activePaths.agentEnvDir, { recursive: true }); const startAgentSessionPath = join(activePaths.fleetToolsDir, 'start-agent-session.sh'); + const startInteractionServicePath = join( + activePaths.fleetToolsDir, + 'start-interaction-service.sh', + ); + const printInteractionPolicyPath = join( + activePaths.fleetToolsDir, + 'print-interaction-effective-policy.sh', + ); const sendMessagePath = join(activePaths.tmuxToolsDir, 'send-message.sh'); const agentSendPath = join(activePaths.tmuxToolsDir, 'agent-send.sh'); - const executableToolPaths = [startAgentSessionPath, sendMessagePath, agentSendPath]; + const executableToolPaths = [ + startAgentSessionPath, + startInteractionServicePath, + printInteractionPolicyPath, + sendMessagePath, + agentSendPath, + ]; await copyFile( join(frameworkRoot, 'tools', 'fleet', 'start-agent-session.sh'), startAgentSessionPath, ); + await copyFile( + join(frameworkRoot, 'tools', 'fleet', 'start-interaction-service.sh'), + startInteractionServicePath, + ); + await copyFile( + join(frameworkRoot, 'tools', 'fleet', 'print-interaction-effective-policy.sh'), + printInteractionPolicyPath, + ); await copyFile(join(frameworkRoot, 'tools', 'tmux', 'send-message.sh'), sendMessagePath); await copyFile(join(frameworkRoot, 'tools', 'tmux', 'agent-send.sh'), agentSendPath); for (const toolPath of executableToolPaths) { @@ -2336,6 +2370,10 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise join(frameworkRoot, 'systemd', 'user', 'mosaic-agent@.service'), join(activePaths.systemdUserDir, 'mosaic-agent@.service'), ); + await copyFile( + join(frameworkRoot, 'systemd', 'user', 'mosaic-interaction-agent@.service'), + join(activePaths.systemdUserDir, 'mosaic-interaction-agent@.service'), + ); for (const agent of roster.agents) { const envPath = join(activePaths.agentEnvDir, `${agent.name}.env`); @@ -2462,6 +2500,10 @@ function normalizeAgent(raw: NonNullable[number]): Fle 'workingDirectory', 'model_hint', 'modelHint', + 'reasoning_level', + 'reasoningLevel', + 'tool_policy', + 'toolPolicy', 'persistent_persona', 'persistentPersona', 'reset_between_tasks', @@ -2493,6 +2535,14 @@ function normalizeAgent(raw: NonNullable[number]): Fle raw.model_hint ?? raw.modelHint, `Fleet roster agent "${name}" model_hint`, ), + reasoningLevel: optionalString( + raw.reasoning_level ?? raw.reasoningLevel, + `Fleet roster agent "${name}" reasoning_level`, + ), + toolPolicy: optionalString( + raw.tool_policy ?? raw.toolPolicy, + `Fleet roster agent "${name}" tool_policy`, + ), persistentPersona: optionalBooleanOrString( raw.persistent_persona ?? raw.persistentPersona, `Fleet roster agent "${name}" persistent_persona`, @@ -2783,6 +2833,12 @@ export function serializeRosterToYaml(roster: FleetRoster): string { if (agent.modelHint !== undefined) { raw['model_hint'] = agent.modelHint; } + if (agent.reasoningLevel !== undefined) { + raw['reasoning_level'] = agent.reasoningLevel; + } + if (agent.toolPolicy !== undefined) { + raw['tool_policy'] = agent.toolPolicy; + } if (agent.persistentPersona !== undefined) { raw['persistent_persona'] = agent.persistentPersona; } diff --git a/packages/mosaic/src/commands/interaction.test.ts b/packages/mosaic/src/commands/interaction.test.ts new file mode 100644 index 0000000..856a375 --- /dev/null +++ b/packages/mosaic/src/commands/interaction.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; +import { Command } from 'commander'; +import { registerInteractionCommand } from './interaction.js'; + +describe('generic interaction CLI', (): void => { + it('registers every required interaction verb without an instance-specific command name', () => { + const program = new Command(); + const command = registerInteractionCommand(program); + expect(command.name()).toBe('interaction'); + expect(command.commands.map((item) => item.name()).sort()).toEqual([ + 'attach', + 'chat', + 'enroll', + 'health', + 'recover', + 'send', + 'sessions', + 'status', + 'stop', + 'tree', + ]); + }); +}); diff --git a/packages/mosaic/src/commands/interaction.ts b/packages/mosaic/src/commands/interaction.ts new file mode 100644 index 0000000..2e543d0 --- /dev/null +++ b/packages/mosaic/src/commands/interaction.ts @@ -0,0 +1,232 @@ +import { randomUUID } from 'node:crypto'; +import type { Command } from 'commander'; +import { withAuth } from './with-auth.js'; +import { + attachInteractionSession, + enrollInteractionSession, + fetchInteractionHealth, + fetchInteractionSessions, + fetchInteractionStatus, + fetchInteractionTree, + recoverInteractionSession, + sendInteractionMessage, + stopInteractionSession, + streamInteractionSession, +} from '../tui/gateway-api.js'; + +interface InteractionOptions { + gateway: string; + agent?: string; + correlationId?: string; +} + +function configuredAgentName(options: InteractionOptions): string { + const name = options.agent?.trim() || process.env['MOSAIC_AGENT_NAME']?.trim(); + if (!name) throw new Error('Interaction agent name is required (--agent or MOSAIC_AGENT_NAME)'); + return name; +} + +async function authenticatedGateway(options: InteractionOptions) { + return withAuth(options.gateway); +} + +async function authRequest(options: InteractionOptions) { + const auth = await authenticatedGateway(options); + return { + gateway: auth.gateway, + cookie: auth.cookie, + agentName: configuredAgentName(options), + correlationId: options.correlationId?.trim() || randomUUID(), + }; +} + +function options(command: Command): Command { + return command + .option('-g, --gateway ', 'Gateway URL', 'http://localhost:14242') + .option('--agent ', 'Configured interaction agent name') + .option('--correlation-id ', 'Correlation ID for audit tracing'); +} + +function print(value: unknown): void { + console.log(JSON.stringify(value, null, 2)); +} + +/** + * Generic interaction command surface. The deployed instance name is data + * (`--agent` / MOSAIC_AGENT_NAME), not a source-code command literal. + */ +export function registerInteractionCommand(program: Command): Command { + const command = program + .command('interaction') + .description('Operate a configured durable interaction agent') + .configureHelp({ sortSubcommands: true }); + + options( + command + .command('status') + .description('Show credential-safe effective policy and provider status'), + ).action(async (opts: InteractionOptions) => { + const auth = await authenticatedGateway(opts); + print(await fetchInteractionStatus(auth.gateway, auth.cookie)); + }); + + options( + command + .command('health') + .description('Show readiness without configuration or credential data'), + ).action(async (opts: InteractionOptions) => { + print(await fetchInteractionHealth(opts.gateway)); + }); + + options( + command + .command('sessions ') + .description('List provider sessions visible to this actor'), + ).action(async (provider: string, opts: InteractionOptions) => { + const request = await authRequest(opts); + print( + await fetchInteractionSessions(request.gateway, request.cookie, { + ...request, + providerId: provider, + }), + ); + }); + + options( + command.command('tree ').description('Show the provider session hierarchy'), + ).action(async (provider: string, opts: InteractionOptions) => { + const request = await authRequest(opts); + print( + await fetchInteractionTree(request.gateway, request.cookie, { + ...request, + providerId: provider, + }), + ); + }); + + options( + command + .command('enroll ') + .description('Bind an authorized runtime session to a durable conversation handle'), + ).action( + async ( + sessionId: string, + providerId: string, + runtimeSessionId: string, + opts: InteractionOptions, + ) => { + const request = await authRequest(opts); + print( + await enrollInteractionSession(request.gateway, request.cookie, { + ...request, + sessionId, + providerId, + runtimeSessionId, + }), + ); + }, + ); + + options( + command + .command('attach ') + .description('Create a scoped read or write attachment and stream the session'), + ) + .option('--control', 'Request control mode (provider policy may deny it)') + .action(async (sessionId: string, opts: InteractionOptions & { control?: boolean }) => { + const request = await authRequest(opts); + const attachment = await attachInteractionSession(request.gateway, request.cookie, { + ...request, + sessionId, + mode: opts.control ? 'control' : 'read', + }); + print(attachment); + for await (const event of streamInteractionSession(request.gateway, request.cookie, { + ...request, + sessionId, + })) { + print(event); + } + }); + + const send = options( + command + .command('send ') + .description('Durably queue and dispatch a message'), + ) + .option('--idempotency-key ', 'Stable idempotency key') + .action( + async ( + sessionId: string, + message: string, + opts: InteractionOptions & { idempotencyKey?: string }, + ) => { + const request = await authRequest(opts); + print( + await sendInteractionMessage(request.gateway, request.cookie, { + ...request, + sessionId, + content: message, + idempotencyKey: opts.idempotencyKey?.trim() || randomUUID(), + }), + ); + }, + ); + void send; + + options( + command + .command('chat ') + .description('Send a chat message through the durable interaction session'), + ) + .option('--idempotency-key ', 'Stable idempotency key') + .action( + async ( + sessionId: string, + message: string, + opts: InteractionOptions & { idempotencyKey?: string }, + ) => { + const request = await authRequest(opts); + print( + await sendInteractionMessage(request.gateway, request.cookie, { + ...request, + sessionId, + content: message, + idempotencyKey: opts.idempotencyKey?.trim() || randomUUID(), + }), + ); + }, + ); + + options( + command + .command('stop ') + .description('Terminate a session using a one-time exact-action approval'), + ) + .requiredOption('--approval ', 'Durable exact-action approval reference') + .action(async (sessionId: string, opts: InteractionOptions & { approval: string }) => { + const request = await authRequest(opts); + print( + await stopInteractionSession(request.gateway, request.cookie, { + ...request, + sessionId, + approvalRef: opts.approval, + }), + ); + }); + + options( + command + .command('recover ') + .description( + 'Recover durable inbox/checkpoint state without replaying ambiguous outbox effects', + ), + ).action(async (sessionId: string, opts: InteractionOptions) => { + const request = await authRequest(opts); + print( + await recoverInteractionSession(request.gateway, request.cookie, { ...request, sessionId }), + ); + }); + + return command; +} diff --git a/packages/mosaic/src/commands/launch.ts b/packages/mosaic/src/commands/launch.ts index fabff2a..4d96f9a 100644 --- a/packages/mosaic/src/commands/launch.ts +++ b/packages/mosaic/src/commands/launch.ts @@ -395,6 +395,9 @@ For required push/merge/issue-close/release actions, execute without routine con const persona = readPersonaContractBlock(mosaicHome, process.env['MOSAIC_AGENT_CLASS']); if (persona) parts.push('\n\n' + persona); + const toolPolicy = readFleetToolPolicyBlock(process.env['MOSAIC_AGENT_TOOL_POLICY']); + if (toolPolicy) parts.push('\n\n' + toolPolicy); + // Fleet onboarding: when this is a spawned fleet agent (MOSAIC_AGENT_NAME set // and present in the roster), inject a comms cheat-sheet + peer roster so it // knows how to reach the orchestrator and its peers from its first turn. @@ -404,6 +407,17 @@ For required push/merge/issue-close/release actions, execute without routine con return parts.join('\n'); } +function readFleetToolPolicyBlock(policy: string | undefined): string { + if (policy !== 'operator-interaction') return ''; + return [ + '# Fleet Tool Policy (operator-interaction)', + '', + 'Permitted: authorized conversation, status, retrieval, and safe diagnostics.', + 'Denied by default: coding/general orchestration claims, direct fleet control, destructive actions, and credential access.', + 'Delegate Mos-owned work through the authorized handoff boundary.', + ].join('\n'); +} + /** @deprecated internal alias — use composeContract. Retained for call-site clarity. */ function buildRuntimePrompt(runtime: RuntimeName): string { return composeContract(runtime); diff --git a/packages/mosaic/src/fleet/interaction-service-profile.ts b/packages/mosaic/src/fleet/interaction-service-profile.ts new file mode 100644 index 0000000..d22fccb --- /dev/null +++ b/packages/mosaic/src/fleet/interaction-service-profile.ts @@ -0,0 +1,110 @@ +import { readFile } from 'node:fs/promises'; +import YAML from 'yaml'; +import type { FleetAgent } from '../commands/fleet.js'; + +const REQUIRED_RUNTIME = 'pi'; +const REQUIRED_MODEL = 'openai/gpt-5.6-sol'; +const REQUIRED_REASONING = 'high'; +const REQUIRED_TOOL_POLICY = 'operator-interaction'; +const AGENT_NAME_PATTERN = /^[A-Za-z0-9_.-]+$/; + +export interface InteractionServiceProfile { + runtime: string; + model: string; + reasoning: string; + toolPolicy: string; +} + +export interface EffectiveInteractionPolicy { + agentName: string; + runtime: string; + model: string; + reasoning: string; + toolPolicy: string; +} + +export class InteractionServiceProfileError extends Error { + constructor( + readonly code: 'invalid_profile' | 'invalid_request', + message: string, + ) { + super(message); + this.name = InteractionServiceProfileError.name; + } +} + +export async function readInteractionServiceProfile( + path: string, + overrides: Partial = {}, +): Promise { + const parsed: unknown = YAML.parse(await readFile(path, 'utf8')); + if (!isRecord(parsed)) { + throw new InteractionServiceProfileError( + 'invalid_profile', + 'Service profile must be an object', + ); + } + const profile: InteractionServiceProfile = { + runtime: valueOrUndefined(overrides.runtime, parsed['runtime']), + model: valueOrUndefined(overrides.model, parsed['model']), + reasoning: valueOrUndefined(overrides.reasoning, parsed['reasoning']), + toolPolicy: valueOrUndefined(overrides.toolPolicy, parsed['tool_policy']), + }; + assertPinnedPolicy(profile); + return profile; +} + +export function provisionInteractionService( + profile: InteractionServiceProfile, + input: { agentName: string }, +): { rosterAgent: FleetAgent; effectivePolicy: EffectiveInteractionPolicy } { + assertPinnedPolicy(profile); + if (!AGENT_NAME_PATTERN.test(input.agentName)) { + throw new InteractionServiceProfileError( + 'invalid_request', + 'Agent name must contain only letters, numbers, dots, underscores, or hyphens', + ); + } + const effectivePolicy: EffectiveInteractionPolicy = { + agentName: input.agentName, + runtime: profile.runtime, + model: profile.model, + reasoning: profile.reasoning, + toolPolicy: profile.toolPolicy, + }; + return { + rosterAgent: { + name: input.agentName, + runtime: profile.runtime, + className: profile.toolPolicy, + modelHint: profile.model, + reasoningLevel: profile.reasoning, + toolPolicy: profile.toolPolicy, + persistentPersona: true, + }, + effectivePolicy, + }; +} + +function assertPinnedPolicy(profile: InteractionServiceProfile): void { + const invalid = + profile.runtime !== REQUIRED_RUNTIME || + profile.model !== REQUIRED_MODEL || + profile.reasoning !== REQUIRED_REASONING || + profile.toolPolicy !== REQUIRED_TOOL_POLICY; + if (invalid) { + throw new InteractionServiceProfileError( + 'invalid_profile', + `Service profile must pin ${REQUIRED_RUNTIME}, ${REQUIRED_MODEL}, ${REQUIRED_REASONING}, and ${REQUIRED_TOOL_POLICY}`, + ); + } +} + +function valueOrUndefined(override: string | undefined, value: unknown): string { + if (override !== undefined) return override; + return typeof value === 'string' ? value : ''; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/mosaic/src/fleet/tess-service-profile.test.ts b/packages/mosaic/src/fleet/tess-service-profile.test.ts new file mode 100644 index 0000000..5c651a5 --- /dev/null +++ b/packages/mosaic/src/fleet/tess-service-profile.test.ts @@ -0,0 +1,161 @@ +import { execFile } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; +import { describe, expect, it } from 'vitest'; +import { generateAgentEnv, loadFleetRoster } from '../commands/fleet.js'; +import { + provisionInteractionService, + readInteractionServiceProfile, +} from './interaction-service-profile.js'; +import type { InteractionServiceProfileError } from './interaction-service-profile.js'; + +const frameworkFleet = resolve( + dirname(fileURLToPath(import.meta.url)), + '..', + '..', + 'framework', + 'fleet', +); +const profilePath = join(frameworkFleet, 'services', 'operator-interaction.yaml'); +const examplePath = join(frameworkFleet, 'examples', 'operator-interaction.yaml'); +const policyScript = join( + frameworkFleet, + '..', + 'tools', + 'fleet', + 'print-interaction-effective-policy.sh', +); +const interactionStartScript = join( + frameworkFleet, + '..', + 'tools', + 'fleet', + 'start-interaction-service.sh', +); +const agentStartScript = join(frameworkFleet, '..', 'tools', 'fleet', 'start-agent-session.sh'); +const execFileAsync = promisify(execFile); + +describe('operator interaction service profile', (): void => { + it('provisions a user-supplied Nova identity as data with the required effective policy', async (): Promise => { + const profile = await readInteractionServiceProfile(profilePath); + const provisioned = provisionInteractionService(profile, { agentName: 'Nova' }); + + expect(provisioned.rosterAgent).toEqual({ + name: 'Nova', + runtime: 'pi', + className: 'operator-interaction', + modelHint: 'openai/gpt-5.6-sol', + reasoningLevel: 'high', + toolPolicy: 'operator-interaction', + persistentPersona: true, + }); + expect(provisioned.effectivePolicy).toEqual({ + agentName: 'Nova', + runtime: 'pi', + model: 'openai/gpt-5.6-sol', + reasoning: 'high', + toolPolicy: 'operator-interaction', + }); + expect(JSON.stringify(provisioned.effectivePolicy)).not.toMatch( + /token|secret|credential|api.?key/i, + ); + }); + + it('accepts a renamed example roster and surfaces only its effective policy', async (): Promise => { + const directory = await mkdtemp(join(tmpdir(), 'mosaic-interaction-profile-')); + const rosterPath = join(directory, 'roster.yaml'); + try { + const example = await readFile(examplePath, 'utf8'); + await writeFile(rosterPath, example.replace('name: Tess', 'name: Nova'), 'utf8'); + const roster = await loadFleetRoster(rosterPath); + const agent = roster.agents[0]!; + + expect(agent.name).toBe('Nova'); + expect(agent.reasoningLevel).toBe('high'); + expect(agent.toolPolicy).toBe('operator-interaction'); + expect(generateAgentEnv(roster, agent)).toContain('MOSAIC_AGENT_NAME=Nova'); + expect(generateAgentEnv(roster, agent)).toContain('MOSAIC_AGENT_REASONING=high'); + + const { stdout } = await execFileAsync(policyScript, [], { + env: { + ...process.env, + MOSAIC_AGENT_NAME: agent.name, + MOSAIC_AGENT_RUNTIME: agent.runtime, + MOSAIC_AGENT_MODEL: agent.modelHint, + MOSAIC_AGENT_REASONING: agent.reasoningLevel, + MOSAIC_AGENT_TOOL_POLICY: agent.toolPolicy, + }, + }); + expect(JSON.parse(stdout)).toEqual({ + agentName: 'Nova', + runtime: 'pi', + model: 'openai/gpt-5.6-sol', + reasoning: 'high', + toolPolicy: 'operator-interaction', + }); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); + + it('fails before launch when service environment drifts from the pinned policy', async (): Promise => { + await expect( + execFileAsync(interactionStartScript, ['Nova'], { + env: { + ...process.env, + MOSAIC_AGENT_NAME: 'Nova', + MOSAIC_AGENT_RUNTIME: 'pi', + MOSAIC_AGENT_MODEL: 'other/model', + MOSAIC_AGENT_REASONING: 'high', + MOSAIC_AGENT_TOOL_POLICY: 'operator-interaction', + }, + }), + ).rejects.toMatchObject({ code: 64 }); + }); + + it('rejects unsafe names from the policy printer and reasoning before tmux launch', async (): Promise => { + await expect( + execFileAsync(policyScript, [], { + env: { + ...process.env, + MOSAIC_AGENT_NAME: 'Nova"bad', + MOSAIC_AGENT_RUNTIME: 'pi', + MOSAIC_AGENT_MODEL: 'openai/gpt-5.6-sol', + MOSAIC_AGENT_REASONING: 'high', + MOSAIC_AGENT_TOOL_POLICY: 'operator-interaction', + }, + }), + ).rejects.toMatchObject({ code: 64 }); + await expect( + execFileAsync(agentStartScript, ['Nova'], { + env: { ...process.env, MOSAIC_AGENT_REASONING: 'high; id' }, + }), + ).rejects.toMatchObject({ code: 64 }); + }); + + it('keeps the product name confined to an example instance, not service source or defaults', async (): Promise => { + const [profile, source, example] = await Promise.all([ + readFile(profilePath, 'utf8'), + readFile(new URL('./interaction-service-profile.ts', import.meta.url), 'utf8'), + readFile(examplePath, 'utf8'), + ]); + + expect(profile).not.toMatch(/tess/i); + expect(source).not.toMatch(/tess/i); + expect(example).toMatch(/name: Tess/); + }); + + it('fails fast when a required policy field is absent or changed from the pinned policy', async (): Promise => { + await expect(readInteractionServiceProfile(profilePath, { model: '' })).rejects.toMatchObject({ + code: 'invalid_profile', + } satisfies Partial); + await expect( + readInteractionServiceProfile(profilePath, { reasoning: 'medium' }), + ).rejects.toMatchObject({ + code: 'invalid_profile', + } satisfies Partial); + }); +}); diff --git a/packages/mosaic/src/index.ts b/packages/mosaic/src/index.ts index cc881e5..4ff4162 100644 --- a/packages/mosaic/src/index.ts +++ b/packages/mosaic/src/index.ts @@ -1,5 +1,6 @@ export const VERSION = '0.0.0'; +export * from './fleet/interaction-service-profile.js'; export * from './fleet/tmux-runtime-transport.js'; export { diff --git a/packages/mosaic/src/tui/gateway-api.ts b/packages/mosaic/src/tui/gateway-api.ts index 34147ab..7e169bd 100644 --- a/packages/mosaic/src/tui/gateway-api.ts +++ b/packages/mosaic/src/tui/gateway-api.ts @@ -361,6 +361,207 @@ export async function deleteMission( } } +// ── Authenticated interaction runtime endpoints ── + +export interface InteractionRequest { + agentName: string; + correlationId?: string; +} + +function interactionHeaders(sessionCookie: string, gatewayUrl: string, correlationId?: string) { + return { + ...jsonHeaders(sessionCookie, gatewayUrl), + ...(correlationId ? { 'X-Correlation-Id': correlationId } : {}), + }; +} + +function interactionPath(agentName: string, suffix: string): string { + return `/api/interaction/${encodeURIComponent(agentName)}${suffix}`; +} + +export async function fetchInteractionSessions( + gatewayUrl: string, + sessionCookie: string, + request: InteractionRequest & { providerId: string }, +): Promise { + const params = new URLSearchParams({ provider: request.providerId }); + const res = await fetch( + `${gatewayUrl}${interactionPath(request.agentName, `/sessions?${params}`)}`, + { + headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId), + }, + ); + return handleResponse(res, 'Failed to list interaction sessions'); +} + +export async function fetchInteractionTree( + gatewayUrl: string, + sessionCookie: string, + request: InteractionRequest & { providerId: string }, +): Promise { + const params = new URLSearchParams({ provider: request.providerId }); + const res = await fetch(`${gatewayUrl}${interactionPath(request.agentName, `/tree?${params}`)}`, { + headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId), + }); + return handleResponse(res, 'Failed to get interaction session tree'); +} + +export async function enrollInteractionSession( + gatewayUrl: string, + sessionCookie: string, + request: InteractionRequest & { + sessionId: string; + providerId: string; + runtimeSessionId: string; + }, +): Promise<{ status: string; sessionId: string }> { + const res = await fetch( + `${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/enroll`)}`, + { + method: 'POST', + headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId), + body: JSON.stringify({ + providerId: request.providerId, + runtimeSessionId: request.runtimeSessionId, + }), + }, + ); + return handleResponse<{ status: string; sessionId: string }>( + res, + 'Failed to enroll interaction session', + ); +} + +export async function attachInteractionSession( + gatewayUrl: string, + sessionCookie: string, + request: InteractionRequest & { sessionId: string; mode?: 'read' | 'control' }, +): Promise { + const res = await fetch( + `${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/attach`)}`, + { + method: 'POST', + headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId), + body: JSON.stringify({ mode: request.mode ?? 'read' }), + }, + ); + return handleResponse(res, 'Failed to attach interaction session'); +} + +export async function* streamInteractionSession( + gatewayUrl: string, + sessionCookie: string, + request: InteractionRequest & { sessionId: string; cursor?: string }, +): AsyncIterable { + const params = request.cursor?.trim() + ? `?${new URLSearchParams({ cursor: request.cursor.trim() }).toString()}` + : ''; + const res = await fetch( + `${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/stream${params}`)}`, + { headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId) }, + ); + if (!res.ok || !res.body) { + const body = await res.text().catch(() => ''); + throw new Error(`Failed to stream interaction session (${res.status}): ${body}`); + } + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let pending = ''; + try { + for (;;) { + const chunk = await reader.read(); + if (chunk.done) break; + pending += decoder.decode(chunk.value, { stream: true }); + for (;;) { + const separator = pending.indexOf('\n\n'); + if (separator < 0) break; + const frame = pending.slice(0, separator); + pending = pending.slice(separator + 2); + const data = frame + .split('\n') + .find((line: string): boolean => line.startsWith('data:')) + ?.slice('data:'.length) + .trim(); + if (data) yield JSON.parse(data) as unknown; + } + } + } finally { + reader.releaseLock(); + } +} + +export async function sendInteractionMessage( + gatewayUrl: string, + sessionCookie: string, + request: InteractionRequest & { sessionId: string; content: string; idempotencyKey: string }, +): Promise<{ status: string; sessionId: string }> { + const res = await fetch( + `${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/send`)}`, + { + method: 'POST', + headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId), + body: JSON.stringify({ content: request.content, idempotencyKey: request.idempotencyKey }), + }, + ); + return handleResponse<{ status: string; sessionId: string }>( + res, + 'Failed to send interaction message', + ); +} + +export async function stopInteractionSession( + gatewayUrl: string, + sessionCookie: string, + request: InteractionRequest & { sessionId: string; approvalRef: string }, +): Promise<{ status: string; sessionId: string }> { + const res = await fetch( + `${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/stop`)}`, + { + method: 'POST', + headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId), + body: JSON.stringify({ approvalRef: request.approvalRef }), + }, + ); + return handleResponse<{ status: string; sessionId: string }>( + res, + 'Failed to stop interaction session', + ); +} + +export async function recoverInteractionSession( + gatewayUrl: string, + sessionCookie: string, + request: InteractionRequest & { sessionId: string }, +): Promise<{ status: string; sessionId: string }> { + const res = await fetch( + `${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/recover`)}`, + { + method: 'POST', + headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId), + }, + ); + return handleResponse<{ status: string; sessionId: string }>( + res, + 'Failed to recover interaction session', + ); +} + +export async function fetchInteractionStatus( + gatewayUrl: string, + sessionCookie: string, +): Promise { + const res = await fetch(`${gatewayUrl}/api/providers/status`, { + headers: headers(sessionCookie, gatewayUrl), + }); + return handleResponse(res, 'Failed to get effective interaction policy'); +} + +export async function fetchInteractionHealth(gatewayUrl: string): Promise { + const res = await fetch(`${gatewayUrl}/health/ready`); + return handleResponse(res, 'Failed to get interaction readiness'); +} + // ── Conversation Message types ── export interface ConversationMessage { diff --git a/packages/types/src/agent/agent-runtime-provider.ts b/packages/types/src/agent/agent-runtime-provider.ts index 4a5fb56..c5b343a 100644 --- a/packages/types/src/agent/agent-runtime-provider.ts +++ b/packages/types/src/agent/agent-runtime-provider.ts @@ -6,6 +6,24 @@ export type RuntimeCapability = | 'session.attach' | 'session.terminate'; export type RuntimeSessionState = 'starting' | 'active' | 'idle' | 'stopped' | 'failed'; + +/** Transitional capability inventory is normalized; provider legacy vocabularies never enter core. */ +export type TransitionalRuntimeCapability = 'kanban' | 'skills' | 'memory' | 'tools' | 'cron'; +export type TransitionalCapabilityStatus = 'supported' | 'unsupported'; +export interface TransitionalCapabilityInventoryEntry { + capability: TransitionalRuntimeCapability; + status: TransitionalCapabilityStatus; +} +/** Optional extension for transitional adapters; not every runtime has Hermes inventory. */ +export interface TransitionalCapabilityInventoryProvider { + transitionalCapabilityMatrix( + scope: RuntimeScope, + ): Promise; + assertTransitionalCapability( + capability: TransitionalRuntimeCapability, + scope: RuntimeScope, + ): Promise; +} export type RuntimeAttachMode = 'read' | 'control'; /** Server-derived immutable authority context. Client identity fields are intentionally absent. */ diff --git a/plugins/discord/src/index.ts b/plugins/discord/src/index.ts index d25e555..15d4997 100644 --- a/plugins/discord/src/index.ts +++ b/plugins/discord/src/index.ts @@ -12,6 +12,129 @@ export interface DiscordPluginConfig { allowedGuildIds: readonly string[]; allowedChannelIds: readonly string[]; allowedUserIds: readonly string[]; + /** Provisioned interaction bindings; instance identity is configuration, never code. */ + interactionBindings?: readonly DiscordInteractionBinding[]; +} + +export type DiscordInteractionOperation = 'bind' | 'attach' | 'send' | 'approve' | 'stop'; +export type DiscordInteractionRole = 'viewer' | 'operator' | 'admin'; + +/** A provisioned Discord-to-Mosaic identity pairing. */ +export interface DiscordInteractionUserBinding { + role: DiscordInteractionRole; + /** Required for privileged approval and stop operations. */ + mosaicUserId?: string; +} + +/** Legacy role-only pairings remain valid for non-privileged Discord ingress. */ +export type DiscordInteractionPairing = DiscordInteractionRole | DiscordInteractionUserBinding; + +export interface DiscordInteractionBinding { + instanceId: string; + guildId: string; + channelId: string; + /** Pairing roster keyed by Discord user ID. */ + pairedUsers: Readonly>; +} + +const operationRoles: Readonly< + Record +> = { + bind: ['admin'], + attach: ['operator', 'admin'], + send: ['operator', 'admin'], + approve: ['admin'], + stop: ['admin'], +}; + +/** Resolves a configuration-owned binding and applies pairing/RBAC before ingress. */ +export function resolveDiscordInteractionBinding( + bindings: readonly DiscordInteractionBinding[], + guildId: string, + channelId: string, + userId: string, + operation: DiscordInteractionOperation, +): DiscordInteractionBinding | null { + const binding = bindings.find( + (candidate) => candidate.guildId === guildId && candidate.channelId === channelId, + ); + if (!binding) return null; + const pairing = binding.pairedUsers[userId]; + const role = typeof pairing === 'string' ? pairing : pairing?.role; + return role && operationRoles[operation].includes(role) ? binding : null; +} + +/** Resolves the provisioned Mosaic identity for an already-authorized Discord user. */ +export function resolveDiscordInteractionActorId( + binding: DiscordInteractionBinding, + discordUserId: string, +): string | null { + const pairing = binding.pairedUsers[discordUserId]; + if (typeof pairing === 'string') return null; + const mosaicUserId = pairing?.mosaicUserId?.trim(); + return mosaicUserId || null; +} + +/** Parses provisioned binding roster JSON and rejects malformed or empty data. */ +export function parseDiscordInteractionBindings( + value: string | undefined, +): DiscordInteractionBinding[] { + if (!value) throw new Error('DISCORD_INTERACTION_BINDINGS is required when Discord is enabled'); + const parsed: unknown = JSON.parse(value); + if (!Array.isArray(parsed) || parsed.length === 0) { + throw new Error('DISCORD_INTERACTION_BINDINGS must be a non-empty JSON array'); + } + return parsed.map((binding: unknown): DiscordInteractionBinding => { + if (typeof binding !== 'object' || binding === null) + throw new Error('Invalid Discord interaction binding'); + const candidate = binding as Partial; + if ( + !candidate.instanceId || + !candidate.guildId || + !candidate.channelId || + !candidate.pairedUsers || + typeof candidate.pairedUsers !== 'object' + ) { + throw new Error('Invalid Discord interaction binding'); + } + const pairedUsers = Object.fromEntries( + Object.entries(candidate.pairedUsers).map(([discordUserId, pairing]: [string, unknown]) => { + if (!discordUserId.trim()) { + throw new Error('Invalid Discord interaction user binding'); + } + if (typeof pairing === 'string') { + if (!['viewer', 'operator', 'admin'].includes(pairing)) { + throw new Error('Invalid Discord interaction user binding'); + } + return [discordUserId, pairing]; + } + if (typeof pairing !== 'object' || pairing === null) { + throw new Error('Invalid Discord interaction user binding'); + } + const userBinding = pairing as Partial; + if ( + !userBinding.role || + !['viewer', 'operator', 'admin'].includes(userBinding.role) || + (userBinding.mosaicUserId !== undefined && !userBinding.mosaicUserId.trim()) + ) { + throw new Error('Invalid Discord interaction user binding'); + } + return [ + discordUserId, + { + role: userBinding.role, + ...(userBinding.mosaicUserId ? { mosaicUserId: userBinding.mosaicUserId } : {}), + }, + ]; + }), + ) as Record; + return { + instanceId: candidate.instanceId, + guildId: candidate.guildId, + channelId: candidate.channelId, + pairedUsers, + }; + }); } export interface DiscordIngressPayload { @@ -22,6 +145,15 @@ export interface DiscordIngressPayload { userId: string; conversationId: string; content: string; + threadId?: string; + attachments?: readonly DiscordAttachment[]; +} + +export interface DiscordAttachment { + id: string; + name: string; + url: string; + contentType: string | null; } export interface DiscordIngressEnvelope { @@ -44,6 +176,8 @@ function signedPayload(payload: DiscordIngressPayload): string { payload.userId, payload.conversationId, payload.content, + payload.threadId ?? '', + JSON.stringify(payload.attachments ?? []), ].join('\n'); } @@ -214,7 +348,18 @@ export class DiscordPlugin { } const channelId = message.channelId; - const conversationId = this.channelConversations.get(channelId) ?? `discord-${channelId}`; + const parentChannelId = 'parentId' in message.channel ? message.channel.parentId : null; + const bindingChannelId = parentChannelId ?? channelId; + const binding = resolveDiscordInteractionBinding( + this.config.interactionBindings ?? [], + message.guildId, + bindingChannelId, + message.author.id, + 'send', + ); + if (!binding) return; + const conversationId = + this.channelConversations.get(channelId) ?? `${binding.instanceId}:discord:${channelId}`; this.channelConversations.set(channelId, conversationId); const envelope = createDiscordIngressEnvelope( @@ -222,22 +367,39 @@ export class DiscordPlugin { correlationId: randomUUID(), messageId: message.id, guildId: message.guildId, - channelId, + channelId: bindingChannelId, userId: message.author.id, conversationId, content, + threadId: parentChannelId ? channelId : undefined, + attachments: Array.from(message.attachments.values()).map((attachment) => ({ + id: attachment.id, + name: attachment.name, + url: attachment.url, + contentType: attachment.contentType, + })), }, this.config.serviceToken, ); - this.socket.emit('message', envelope); + this.socket.emit( + /^\/approve$/i.test(content) + ? 'discord:approve' + : content.startsWith('/stop ') + ? 'discord:stop' + : 'message', + envelope, + ); } private isAllowedMessage(message: DiscordMessage): boolean { const guildId = message.guildId; + const parentChannelId = 'parentId' in message.channel ? message.channel.parentId : null; + // Threads inherit their authorization boundary from their configured parent. + const authorizationChannelId = parentChannelId ?? message.channelId; return ( guildId !== null && includesId(this.config.allowedGuildIds, guildId) && - includesId(this.config.allowedChannelIds, message.channelId) && + includesId(this.config.allowedChannelIds, authorizationChannelId) && includesId(this.config.allowedUserIds, message.author.id) ); }