From deb617caf7a36032f68decea9f72f1f0dc2498a6 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 13:47:18 +1000 Subject: [PATCH 01/47] fix(audit): hash persisted metadata representation --- packages/audit/src/audit.test.ts | 30 ++++++++++++++++++++- packages/audit/src/index.ts | 6 +++++ packages/database/src/domain-transaction.ts | 7 ++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/audit/src/audit.test.ts b/packages/audit/src/audit.test.ts index 23588d9..aa6e6a3 100644 --- a/packages/audit/src/audit.test.ts +++ b/packages/audit/src/audit.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "vitest"; -import { hashAuditEvent, verifyAuditChain, type HashableAuditEvent } from "./index"; +import { + hashAuditEvent, + normaliseAuditMetadata, + verifyAuditChain, + type HashableAuditEvent, +} from "./index"; const base: HashableAuditEvent = { organisationId: "org", @@ -23,4 +28,27 @@ describe("audit chain", () => { verifyAuditChain([{ ...event, metadata: { safe: false } }]).valid, ).toBe(false); }); + + it("hashes the same JSON representation that PostgreSQL persists", () => { + const metadata = normaliseAuditMetadata({ + operation: "kelpie.timeline.comment", + approvalId: undefined, + nested: { omitted: undefined, kept: true }, + array: [undefined, "kept"], + }); + expect(metadata).toEqual({ + operation: "kelpie.timeline.comment", + nested: { kept: true }, + array: [null, "kept"], + }); + + const event = { ...base, metadata }; + const persisted = JSON.parse(JSON.stringify(event)) as HashableAuditEvent; + expect(hashAuditEvent(event)).toBe(hashAuditEvent(persisted)); + expect( + verifyAuditChain([ + { ...persisted, eventHash: hashAuditEvent(persisted) }, + ]), + ).toEqual({ valid: true }); + }); }); diff --git a/packages/audit/src/index.ts b/packages/audit/src/index.ts index 785d246..342c341 100644 --- a/packages/audit/src/index.ts +++ b/packages/audit/src/index.ts @@ -27,6 +27,12 @@ export function hashAuditEvent(event: HashableAuditEvent): string { return createHash("sha256").update(canonical(event)).digest("hex"); } +export function normaliseAuditMetadata(metadata: unknown): unknown { + const serialised = JSON.stringify(metadata ?? {}); + if (serialised === undefined) return {}; + return JSON.parse(serialised) as unknown; +} + export function verifyAuditChain( events: ReadonlyArray, ): { valid: boolean; brokenAt?: number } { diff --git a/packages/database/src/domain-transaction.ts b/packages/database/src/domain-transaction.ts index 844f7ee..8150386 100644 --- a/packages/database/src/domain-transaction.ts +++ b/packages/database/src/domain-transaction.ts @@ -1,5 +1,5 @@ import { desc, eq, sql } from "drizzle-orm"; -import { hashAuditEvent } from "@muster/audit"; +import { hashAuditEvent, normaliseAuditMetadata } from "@muster/audit"; import type { ActorTypeSchema } from "@muster/contracts"; import type { z } from "zod"; import type { database } from "./index.ts"; @@ -38,6 +38,7 @@ export async function appendAuditEvent(tx: Transaction, input: AuditWrite) { const createdAt = new Date(); const sequence = (previous?.sequence ?? 0) + 1; const previousHash = previous?.eventHash ?? "0".repeat(64); + const metadata = normaliseAuditMetadata(input.metadata); const hashable = { organisationId: input.organisationId, sequence, @@ -47,7 +48,7 @@ export async function appendAuditEvent(tx: Transaction, input: AuditWrite) { targetType: input.targetType, targetId: input.targetId, previousHash, - metadata: input.metadata ?? {}, + metadata, traceId: input.traceId, createdAt: createdAt.toISOString(), }; @@ -56,7 +57,7 @@ export async function appendAuditEvent(tx: Transaction, input: AuditWrite) { await tx.insert(auditEvents).values({ id, ...input, - metadata: input.metadata ?? {}, + metadata, sequence, previousHash, eventHash, From f0135608d7f6cc6b803738920eb0a446034962b5 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 13:46:07 +1000 Subject: [PATCH 02/47] fix: invoke agents from direct messages --- .../src/runtime.integration.test.ts | 216 +++++++++++- apps/agent-gateway/src/runtime.ts | 158 ++++++++- .../app/api/v1/rooms/[id]/messages/route.ts | 56 ++- ...-direct-message-domain.integration.test.ts | 253 ++++++++++++++ apps/web/lib/agent-direct-message-domain.ts | 330 ++++++++++++++++++ 5 files changed, 986 insertions(+), 27 deletions(-) create mode 100644 apps/web/lib/agent-direct-message-domain.integration.test.ts create mode 100644 apps/web/lib/agent-direct-message-domain.ts diff --git a/apps/agent-gateway/src/runtime.integration.test.ts b/apps/agent-gateway/src/runtime.integration.test.ts index aeea457..8d0237e 100644 --- a/apps/agent-gateway/src/runtime.integration.test.ts +++ b/apps/agent-gateway/src/runtime.integration.test.ts @@ -18,13 +18,10 @@ const describeIntegration = integration ? describe.sequential : describe.skip; describe("Codex structured output schema", () => { it("removes unsupported URI formats while preserving authoritative validation", () => { - const generated = z.toJSONSchema( - AgentStructuredOutputSchemas.HuntResult, - { - target: "draft-2020-12", - io: "output", - }, - ); + const generated = z.toJSONSchema(AgentStructuredOutputSchemas.HuntResult, { + target: "draft-2020-12", + io: "output", + }); expect(JSON.stringify(generated)).toContain('"format":"uri"'); expect(JSON.stringify(codexOutputSchemaFor("HuntResult"))).not.toContain( '"format":"uri"', @@ -172,6 +169,42 @@ describeIntegration("durable agent runtime", () => { throw new Error(`Run ${runId} did not reach ${status}`); } + async function directMessageSource(suffix: string) { + const [room] = await database() + .select({ id: schema.rooms.id }) + .from(schema.rooms) + .innerJoin( + schema.roomMemberships, + and( + eq(schema.roomMemberships.organisationId, organisationId), + eq(schema.roomMemberships.roomId, schema.rooms.id), + eq(schema.roomMemberships.actorId, agentId), + ), + ) + .where( + and( + eq(schema.rooms.organisationId, organisationId), + eq(schema.rooms.roomType, "direct"), + ), + ) + .limit(1); + if (!room) throw new Error("Seeded agent direct room required"); + const messageId = newId(); + await database() + .insert(schema.messages) + .values({ + id: messageId, + organisationId, + roomId: room.id, + authorActorId: requestedByActorId, + messageType: "text", + document: { type: "doc", content: [] }, + plainText: `Synthetic direct request ${suffix}`, + idempotencyKey: `runtime-direct-source:${messageId}`, + }); + return { messageId, roomId: room.id }; + } + it("recovers an expired lease without duplicating the run", async () => { const run = await insertRun("restart"); const duplicateId = newId(); @@ -577,4 +610,173 @@ describeIntegration("durable agent runtime", () => { }); expect(message?.relatedAgentRunId).toBe(run.id); }); + + it("projects a completed direct-message run as one linked room reply", async () => { + const source = await directMessageSource("completed"); + const run = await insertRun("direct-completed", { + roomId: source.roomId, + request: { + kind: "direct_message", + sourceMessageId: source.messageId, + humanRequest: "Review the synthetic direct request", + traceId: `integration-direct-${source.messageId}`, + }, + }); + const runtime = new DurableAgentRuntime({ + executionRuntime: "mock", + codexHome: "/tmp/muster-runtime-integration", + mockDelayMs: 10, + }); + await runtime.dispatch(); + await waitFor(run.id, "completed"); + runtime.stop(); + + const [reply, outbox] = await Promise.all([ + database() + .select() + .from(schema.messages) + .where( + eq( + schema.messages.idempotencyKey, + `agent-direct-message-reply:${run.id}`, + ), + ) + .then((rows) => rows[0]), + database() + .select() + .from(schema.outboxEvents) + .where( + eq( + schema.outboxEvents.idempotencyKey, + `room.message.created:agent-direct-message:${run.id}`, + ), + ) + .then((rows) => rows[0]), + ]); + expect(reply).toMatchObject({ + roomId: source.roomId, + threadParentId: source.messageId, + authorActorId: agentId, + messageType: "agent-status", + relatedAgentRunId: run.id, + }); + expect(reply?.document).toMatchObject({ + type: "agent-direct-message-reply", + status: "completed", + sourceMessageId: source.messageId, + agentRunId: run.id, + trust: "agent-analysis", + }); + expect(outbox?.aggregateId).toBe(reply?.id); + }); + + it("projects a failed direct-message run as one linked room reply", async () => { + const source = await directMessageSource("failed"); + const run = await insertRun("direct-failed", { + roomId: source.roomId, + maximumTokenBudget: 1, + request: { + kind: "direct_message", + sourceMessageId: source.messageId, + humanRequest: "Review the synthetic direct request", + traceId: `integration-direct-${source.messageId}`, + }, + }); + const runtime = new DurableAgentRuntime({ + executionRuntime: "mock", + codexHome: "/tmp/muster-runtime-integration", + mockDelayMs: 10, + }); + await runtime.dispatch(); + await waitFor(run.id, "failed"); + runtime.stop(); + + const replies = await database() + .select() + .from(schema.messages) + .where( + eq( + schema.messages.idempotencyKey, + `agent-direct-message-reply:${run.id}`, + ), + ); + expect(replies).toHaveLength(1); + expect(replies[0]).toMatchObject({ + roomId: source.roomId, + threadParentId: source.messageId, + authorActorId: agentId, + messageType: "agent-status", + relatedAgentRunId: run.id, + }); + expect(replies[0]?.document).toMatchObject({ + type: "agent-direct-message-reply", + status: "failed", + sourceMessageId: source.messageId, + agentRunId: run.id, + failureCode: "token_ceiling", + }); + const outbox = await database() + .select() + .from(schema.outboxEvents) + .where( + eq( + schema.outboxEvents.idempotencyKey, + `room.message.created:agent-direct-message:${run.id}`, + ), + ); + expect(outbox).toHaveLength(1); + }); + + it("projects a direct-message kill-switch failure before execution", async () => { + const source = await directMessageSource("kill-switch"); + const run = await insertRun("direct-kill-switch", { + roomId: source.roomId, + request: { + kind: "direct_message", + sourceMessageId: source.messageId, + humanRequest: "Review the synthetic direct request", + traceId: `integration-direct-${source.messageId}`, + }, + }); + await database() + .update(schema.agentDefinitions) + .set({ killSwitch: true }) + .where(eq(schema.agentDefinitions.id, agentId)); + const runtime = new DurableAgentRuntime({ + executionRuntime: "mock", + codexHome: "/tmp/muster-runtime-integration", + mockDelayMs: 10, + }); + try { + await runtime.dispatch(); + await waitFor(run.id, "failed"); + } finally { + runtime.stop(); + await database() + .update(schema.agentDefinitions) + .set({ killSwitch: false }) + .where(eq(schema.agentDefinitions.id, agentId)); + } + + const [reply] = await database() + .select() + .from(schema.messages) + .where( + eq( + schema.messages.idempotencyKey, + `agent-direct-message-reply:${run.id}`, + ), + ); + expect(reply).toMatchObject({ + threadParentId: source.messageId, + messageType: "agent-status", + relatedAgentRunId: run.id, + }); + expect(reply?.document).toMatchObject({ + status: "failed", + failureCode: "agent_kill_switch", + sourceMessageId: source.messageId, + agentRunId: run.id, + }); + }); }); diff --git a/apps/agent-gateway/src/runtime.ts b/apps/agent-gateway/src/runtime.ts index 63c48b7..318019a 100644 --- a/apps/agent-gateway/src/runtime.ts +++ b/apps/agent-gateway/src/runtime.ts @@ -32,15 +32,124 @@ import { z } from "zod"; type AgentRunRow = typeof schema.agentRuns.$inferSelect; type Context = Awaited>; +type Db = ReturnType; +type Tx = Parameters[0]>[0]; type PersistedRequest = { - kind?: "jessie_hunt" | undefined; + kind?: "jessie_hunt" | "direct_message" | undefined; huntId?: string | undefined; huntPlan?: unknown; humanRequest?: string | undefined; + sourceMessageId?: string | undefined; traceId?: string | undefined; }; +function terminalSummary(output: unknown) { + if (!output || typeof output !== "object" || Array.isArray(output)) + return "The agent completed the request with schema-valid output."; + const record = output as Record; + for (const key of ["summary", "narrative", "headline", "title"]) { + const value = record[key]; + if (typeof value === "string" && value.trim()) + return value.trim().slice(0, 10_000); + } + return "The agent completed the request with schema-valid output."; +} + +async function projectDirectMessageTerminalReply( + tx: Tx, + run: AgentRunRow, + request: PersistedRequest, + terminal: + | { + status: "completed"; + output: unknown; + outputHash: string; + outputSchema: AgentStructuredOutputName; + } + | { + status: "failed"; + failureCode: string; + error: string; + }, +) { + if ( + request.kind !== "direct_message" || + !request.sourceMessageId || + !run.roomId + ) + return; + const [source] = await tx + .select({ id: schema.messages.id }) + .from(schema.messages) + .where( + and( + eq(schema.messages.organisationId, run.organisationId), + eq(schema.messages.id, request.sourceMessageId), + eq(schema.messages.roomId, run.roomId), + ), + ) + .limit(1); + if (!source) return; + + const completed = terminal.status === "completed"; + const plainText = completed + ? terminalSummary(terminal.output) + : `The agent could not complete this request (${terminal.failureCode}). ${redactObservationText(terminal.error, { maxStringLength: 2_000 })}`; + const messageId = newId(); + const [message] = await tx + .insert(schema.messages) + .values({ + id: messageId, + organisationId: run.organisationId, + roomId: run.roomId, + threadParentId: request.sourceMessageId, + authorActorId: run.agentId, + messageType: "agent-status", + document: completed + ? { + type: "agent-direct-message-reply", + status: terminal.status, + sourceMessageId: request.sourceMessageId, + agentRunId: run.id, + outputSchema: terminal.outputSchema, + outputHash: terminal.outputHash, + summary: plainText, + trust: "agent-analysis", + } + : { + type: "agent-direct-message-reply", + status: terminal.status, + sourceMessageId: request.sourceMessageId, + agentRunId: run.id, + failureCode: terminal.failureCode, + }, + plainText, + dataClassification: "internal", + relatedInvestigationId: run.investigationId, + relatedAgentRunId: run.id, + idempotencyKey: `agent-direct-message-reply:${run.id}`, + }) + .onConflictDoNothing() + .returning({ id: schema.messages.id }); + if (!message) return; + await writeOutbox(tx, { + organisationId: run.organisationId, + eventType: "room.message.created", + aggregateType: "message", + aggregateId: message.id, + queueName: "muster-outbox", + payload: { + messageId: message.id, + roomId: run.roomId, + threadParentId: request.sourceMessageId, + agentRunId: run.id, + }, + idempotencyKey: `room.message.created:agent-direct-message:${run.id}`, + traceId: redactObservationText(request.traceId ?? `agent-run-${run.id}`), + }); +} + function removeUnsupportedCodexSchemaFormats(value: unknown): unknown { if (Array.isArray(value)) return value.map(removeUnsupportedCodexSchemaFormats); @@ -505,7 +614,10 @@ export class DurableAgentRuntime { const recovered = candidate.status === "running"; const [run] = await database().transaction(async (tx) => { const [definition] = await tx - .select({ killSwitch: schema.agentDefinitions.killSwitch }) + .select({ + status: schema.agentDefinitions.status, + killSwitch: schema.agentDefinitions.killSwitch, + }) .from(schema.agentDefinitions) .where( and( @@ -517,7 +629,11 @@ export class DurableAgentRuntime { ), ) .limit(1); - if (!definition || definition.killSwitch) { + if ( + !definition || + definition.status !== "active" || + definition.killSwitch + ) { const [disabled] = await tx .update(schema.agentRuns) .set({ @@ -543,6 +659,28 @@ export class DurableAgentRuntime { message: "Agent kill switch blocked execution", payload: { failureCode: "agent_kill_switch" }, }); + await appendAuditEvent(tx, { + organisationId: disabled.organisationId, + actorId: disabled.agentId, + actorType: "agent", + action: "agent.run.failed", + targetType: "agent_run", + targetId: disabled.id, + metadata: { failureCode: "agent_kill_switch" }, + traceId: redactObservationText( + this.request(disabled).traceId ?? `agent-run-${disabled.id}`, + ), + }); + await projectDirectMessageTerminalReply( + tx, + disabled, + this.request(disabled), + { + status: "failed", + failureCode: "agent_kill_switch", + error: "Agent is disabled by its kill switch", + }, + ); } return []; } @@ -1092,6 +1230,12 @@ export class DurableAgentRuntime { this.request(run).traceId ?? `agent-run-${run.id}`, ), }); + await projectDirectMessageTerminalReply(tx, run, this.request(run), { + status: "completed", + output: result.output, + outputHash: result.outputHash, + outputSchema: result.schemaName, + }); const [hunt] = await tx .update(schema.huntRuns) .set({ @@ -1265,6 +1409,11 @@ export class DurableAgentRuntime { this.request(run).traceId ?? `agent-run-${run.id}`, ), }); + await projectDirectMessageTerminalReply(tx, run, this.request(run), { + status: "failed", + failureCode: failure.code, + error: failure.message, + }); const [hunt] = await tx .update(schema.huntRuns) .set({ @@ -1327,10 +1476,11 @@ export class DurableAgentRuntime { private request(run: AgentRunRow): PersistedRequest { const parsed = z .object({ - kind: z.literal("jessie_hunt").optional(), + kind: z.enum(["jessie_hunt", "direct_message"]).optional(), huntId: z.uuid().optional(), huntPlan: z.unknown().optional(), humanRequest: z.string().optional(), + sourceMessageId: z.uuid().optional(), traceId: z.string().optional(), }) .safeParse(run.request); diff --git a/apps/web/app/api/v1/rooms/[id]/messages/route.ts b/apps/web/app/api/v1/rooms/[id]/messages/route.ts index 757e4c5..0997be4 100644 --- a/apps/web/app/api/v1/rooms/[id]/messages/route.ts +++ b/apps/web/app/api/v1/rooms/[id]/messages/route.ts @@ -4,6 +4,7 @@ import { PostMessageSchema, RoomService } from "@muster/rooms"; import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; import { enforceApiRateLimit } from "@/lib/api-rate-limit"; import { publishRealtime } from "@/lib/realtime"; +import { AgentDirectMessageDomainService } from "@/lib/agent-direct-message-domain"; import { JessieHuntDomainService } from "@/lib/jessie-hunt-domain"; export async function GET( @@ -48,33 +49,54 @@ export async function POST( roomId: id, }); const result = await new RoomService().postMessage(subject, input, traceId); + let agentInvocation: Awaited< + ReturnType + > = null; + let agentInvocationError: string | null = null; let jessieHunt: Awaited< ReturnType > = null; let jessieHuntError: string | null = null; if (result.created) { try { - jessieHunt = await new JessieHuntDomainService().maybeCreateFromMention( - subject, - { - messageId: result.message.id, - roomId: id, - plainText: input.plainText, - ...(input.relatedInvestigationId !== undefined - ? { - relatedInvestigationId: input.relatedInvestigationId, - } - : {}), - }, - traceId, - ); + agentInvocation = + await new AgentDirectMessageDomainService().maybeQueue( + subject, + { messageId: result.message.id, roomId: id }, + traceId, + ); } catch (error) { - jessieHuntError = redactObservationText( + agentInvocationError = redactObservationText( error instanceof Error ? error.message - : "Jessie could not prepare the hunt.", + : "The direct-message agent could not be queued.", ); } + if (!agentInvocation && !agentInvocationError) { + try { + jessieHunt = + await new JessieHuntDomainService().maybeCreateFromMention( + subject, + { + messageId: result.message.id, + roomId: id, + plainText: input.plainText, + ...(input.relatedInvestigationId !== undefined + ? { + relatedInvestigationId: input.relatedInvestigationId, + } + : {}), + }, + traceId, + ); + } catch (error) { + jessieHuntError = redactObservationText( + error instanceof Error + ? error.message + : "Jessie could not prepare the hunt.", + ); + } + } } const realtimeDelivered = await publishRealtime(subject.organisationId, { type: input.threadParentId @@ -91,6 +113,8 @@ export async function POST( { data: result.message, duplicate: !result.created, + agentInvocation, + agentInvocationError, jessieHunt, jessieHuntError, realtimeDegraded: !realtimeDelivered, diff --git a/apps/web/lib/agent-direct-message-domain.integration.test.ts b/apps/web/lib/agent-direct-message-domain.integration.test.ts new file mode 100644 index 0000000..28aa8ee --- /dev/null +++ b/apps/web/lib/agent-direct-message-domain.integration.test.ts @@ -0,0 +1,253 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { closeDatabase, database, newId, schema } from "@muster/database"; +import { and, eq } from "drizzle-orm"; +import { AgentDirectMessageDomainService } from "./agent-direct-message-domain"; + +const integration = process.env.MUSTER_INTEGRATION_TESTS === "true"; +const describeIntegration = integration ? describe.sequential : describe.skip; + +describeIntegration("agent direct-message invocation", () => { + let organisationId = ""; + let humanActorId = ""; + let agentId = ""; + let roomId = ""; + let subject: { + actorId: string; + organisationId: string; + capabilities: Set; + }; + + beforeAll(async () => { + const humans = await database() + .select() + .from(schema.actors) + .where(eq(schema.actors.actorType, "human")); + const human = humans.find( + (actor) => + Array.isArray(actor.capabilityAssignments) && + actor.capabilityAssignments.includes("agents.invoke"), + ); + if ( + !human || + !Array.isArray(human.capabilityAssignments) || + !human.capabilityAssignments.includes("agents.invoke") + ) { + throw new Error("Bootstrapped agent invoker required"); + } + organisationId = human.organisationId; + humanActorId = human.id; + subject = { + actorId: human.id, + organisationId, + capabilities: new Set(human.capabilityAssignments as any[]), + }; + agentId = newId(); + roomId = newId(); + await database() + .insert(schema.actors) + .values({ + id: agentId, + organisationId, + actorType: "agent", + displayName: "Synthetic DM Agent", + identityReference: `agent:synthetic-dm:${agentId}`, + capabilityAssignments: [], + }); + await database() + .insert(schema.rooms) + .values({ + id: roomId, + organisationId, + name: `synthetic-dm-${roomId}`, + slug: `synthetic-dm-${roomId}`, + displayName: "Synthetic DM Agent", + roomType: "direct", + visibility: "private", + createdByActorId: humanActorId, + }); + await database() + .insert(schema.roomMemberships) + .values([ + { + organisationId, + roomId, + actorId: humanActorId, + membershipRole: "owner", + }, + { + organisationId, + roomId, + actorId: agentId, + membershipRole: "agent_member", + }, + ]); + await database() + .insert(schema.agentDefinitions) + .values({ + id: agentId, + organisationId, + name: `Synthetic DM Agent ${agentId}`, + description: "Synthetic direct-message integration fixture", + runtime: "mock", + model: "synthetic", + ownerActorId: humanActorId, + systemPromptVersion: "synthetic-dm-v1", + allowedRooms: [roomId], + maximumRuntimeSeconds: 30, + maximumTokenBudget: 1_000, + maximumCostCents: 10, + }); + }); + + afterAll(closeDatabase); + + async function sourceMessage(targetRoomId = roomId) { + const id = newId(); + await database() + .insert(schema.messages) + .values({ + id, + organisationId, + roomId: targetRoomId, + authorActorId: humanActorId, + messageType: "text", + document: { type: "doc", content: [] }, + plainText: `Review synthetic evidence ${id}`, + idempotencyKey: `synthetic-dm-source:${id}`, + }); + return id; + } + + it("queues one durable run, event, audit, and outbox idempotently", async () => { + const messageId = await sourceMessage(); + const service = new AgentDirectMessageDomainService(); + const first = await service.maybeQueue( + subject, + { messageId, roomId }, + `trace-${messageId}`, + ); + const replay = await service.maybeQueue( + subject, + { messageId, roomId }, + `trace-replay-${messageId}`, + ); + + expect(first).toMatchObject({ + handled: true, + queued: true, + duplicate: false, + agentId, + status: "queued", + }); + expect(replay).toMatchObject({ + handled: true, + queued: true, + duplicate: true, + agentRunId: + first && first.queued ? first.agentRunId : "missing-agent-run", + }); + if (!first?.queued) throw new Error("Agent run was not queued"); + const [events, outbox, audit] = await Promise.all([ + database() + .select() + .from(schema.agentRunEvents) + .where(eq(schema.agentRunEvents.runId, first.agentRunId)), + database() + .select() + .from(schema.outboxEvents) + .where( + eq( + schema.outboxEvents.idempotencyKey, + `agent.run.queued:${first.agentRunId}`, + ), + ), + database() + .select() + .from(schema.auditEvents) + .where( + and( + eq(schema.auditEvents.organisationId, organisationId), + eq(schema.auditEvents.targetType, "agent_run"), + eq(schema.auditEvents.targetId, first.agentRunId), + eq(schema.auditEvents.action, "agent.run.queued"), + ), + ), + ]); + expect(events).toHaveLength(1); + expect(outbox).toHaveLength(1); + expect(audit).toHaveLength(1); + }); + + it("requires agents.invoke before queueing", async () => { + const messageId = await sourceMessage(); + await expect( + new AgentDirectMessageDomainService().maybeQueue( + { ...subject, capabilities: new Set() }, + { messageId, roomId }, + `trace-${messageId}`, + ), + ).rejects.toThrow("Missing capability: agents.invoke"); + }); + + it("honours the kill switch and allowed-room boundary", async () => { + const service = new AgentDirectMessageDomainService(); + const disabledMessageId = await sourceMessage(); + await database() + .update(schema.agentDefinitions) + .set({ killSwitch: true }) + .where(eq(schema.agentDefinitions.id, agentId)); + expect( + await service.maybeQueue( + subject, + { messageId: disabledMessageId, roomId }, + `trace-${disabledMessageId}`, + ), + ).toMatchObject({ queued: false, reason: "agent_count" }); + + await database() + .update(schema.agentDefinitions) + .set({ killSwitch: false, allowedRooms: [] }) + .where(eq(schema.agentDefinitions.id, agentId)); + const disallowedMessageId = await sourceMessage(); + expect( + await service.maybeQueue( + subject, + { messageId: disallowedMessageId, roomId }, + `trace-${disallowedMessageId}`, + ), + ).toMatchObject({ queued: false, reason: "agent_unavailable" }); + await database() + .update(schema.agentDefinitions) + .set({ allowedRooms: [roomId] }) + .where(eq(schema.agentDefinitions.id, agentId)); + }); + + it("does not handle a non-direct room", async () => { + const [room] = await database() + .select({ id: schema.rooms.id }) + .from(schema.rooms) + .innerJoin( + schema.roomMemberships, + and( + eq(schema.roomMemberships.roomId, schema.rooms.id), + eq(schema.roomMemberships.actorId, humanActorId), + ), + ) + .where( + and( + eq(schema.rooms.organisationId, organisationId), + eq(schema.rooms.roomType, "operations"), + ), + ) + .limit(1); + if (!room) throw new Error("Bootstrapped operations room required"); + const messageId = await sourceMessage(room.id); + await expect( + new AgentDirectMessageDomainService().maybeQueue( + subject, + { messageId, roomId: room.id }, + `trace-${messageId}`, + ), + ).resolves.toBeNull(); + }); +}); diff --git a/apps/web/lib/agent-direct-message-domain.ts b/apps/web/lib/agent-direct-message-domain.ts new file mode 100644 index 0000000..10b561d --- /dev/null +++ b/apps/web/lib/agent-direct-message-domain.ts @@ -0,0 +1,330 @@ +import { createHash } from "node:crypto"; +import { requireCapability, type AuthorisationSubject } from "@muster/authz"; +import { redactObservationText } from "@muster/config"; +import { + appendAuditEvent, + database, + newId, + schema, + writeOutbox, +} from "@muster/database"; +import { and, eq, gt, isNull, or } from "drizzle-orm"; + +type Db = ReturnType; + +export type DirectMessageInvocation = + | { + handled: true; + queued: false; + duplicate: false; + reason: "agent_count" | "agent_unavailable"; + } + | { + handled: true; + queued: true; + duplicate: boolean; + agentId: string; + agentRunId: string; + status: string; + }; + +export class AgentDirectMessageDomainService { + constructor(private readonly db: Db = database()) {} + + async maybeQueue( + subject: AuthorisationSubject, + input: { messageId: string; roomId: string }, + traceId: string, + ): Promise { + const [source] = await this.db + .select({ + plainText: schema.messages.plainText, + relatedInvestigationId: schema.messages.relatedInvestigationId, + }) + .from(schema.messages) + .innerJoin( + schema.rooms, + and( + eq(schema.rooms.organisationId, subject.organisationId), + eq(schema.rooms.id, schema.messages.roomId), + eq(schema.rooms.roomType, "direct"), + isNull(schema.rooms.archivedAt), + ), + ) + .innerJoin( + schema.roomMemberships, + and( + eq(schema.roomMemberships.organisationId, subject.organisationId), + eq(schema.roomMemberships.roomId, schema.messages.roomId), + eq(schema.roomMemberships.actorId, subject.actorId), + or( + isNull(schema.roomMemberships.accessExpiresAt), + gt(schema.roomMemberships.accessExpiresAt, new Date()), + ), + ), + ) + .innerJoin( + schema.actors, + and( + eq(schema.actors.organisationId, subject.organisationId), + eq(schema.actors.id, schema.messages.authorActorId), + eq(schema.actors.id, subject.actorId), + eq(schema.actors.actorType, "human"), + eq(schema.actors.status, "active"), + ), + ) + .where( + and( + eq(schema.messages.organisationId, subject.organisationId), + eq(schema.messages.id, input.messageId), + eq(schema.messages.roomId, input.roomId), + eq(schema.messages.messageType, "text"), + isNull(schema.messages.deletedAt), + ), + ) + .limit(1); + if (!source) return null; + + const members = await this.db + .select({ + actorId: schema.actors.id, + actorStatus: schema.actors.status, + definitionId: schema.agentDefinitions.id, + definitionStatus: schema.agentDefinitions.status, + killSwitch: schema.agentDefinitions.killSwitch, + allowedRooms: schema.agentDefinitions.allowedRooms, + runtime: schema.agentDefinitions.runtime, + model: schema.agentDefinitions.model, + promptVersion: schema.agentDefinitions.systemPromptVersion, + maximumRuntimeSeconds: schema.agentDefinitions.maximumRuntimeSeconds, + maximumTokenBudget: schema.agentDefinitions.maximumTokenBudget, + maximumCostCents: schema.agentDefinitions.maximumCostCents, + }) + .from(schema.roomMemberships) + .innerJoin( + schema.actors, + and( + eq(schema.actors.organisationId, subject.organisationId), + eq(schema.actors.id, schema.roomMemberships.actorId), + eq(schema.actors.actorType, "agent"), + ), + ) + .leftJoin( + schema.agentDefinitions, + and( + eq(schema.agentDefinitions.organisationId, subject.organisationId), + eq(schema.agentDefinitions.id, schema.actors.id), + ), + ) + .where( + and( + eq(schema.roomMemberships.organisationId, subject.organisationId), + eq(schema.roomMemberships.roomId, input.roomId), + or( + isNull(schema.roomMemberships.accessExpiresAt), + gt(schema.roomMemberships.accessExpiresAt, new Date()), + ), + ), + ); + if (members.length === 0) return null; + + const active = members.filter( + (member) => + member.actorStatus === "active" && + member.definitionId !== null && + member.definitionStatus === "active" && + member.killSwitch === false, + ); + if (active.length !== 1) { + return { + handled: true, + queued: false, + duplicate: false, + reason: "agent_count", + }; + } + const agent = active[0]!; + if ( + !Array.isArray(agent.allowedRooms) || + !agent.allowedRooms.includes(input.roomId) || + !agent.definitionId || + !agent.runtime || + !agent.model || + !agent.promptVersion || + agent.maximumRuntimeSeconds === null || + agent.maximumTokenBudget === null || + agent.maximumCostCents === null + ) { + return { + handled: true, + queued: false, + duplicate: false, + reason: "agent_unavailable", + }; + } + + const agentId = agent.definitionId; + const runtime = agent.runtime; + const model = agent.model; + const promptVersion = agent.promptVersion; + const maximumRuntimeSeconds = agent.maximumRuntimeSeconds; + const maximumTokenBudget = agent.maximumTokenBudget; + const maximumCostCents = agent.maximumCostCents; + requireCapability(subject, "agents.invoke"); + const idempotencyKey = `agent-direct-message:${input.messageId}`; + const inputHash = createHash("sha256") + .update( + JSON.stringify({ + messageId: input.messageId, + roomId: input.roomId, + plainText: source.plainText, + }), + ) + .digest("hex"); + const deadlineAt = new Date(Date.now() + maximumRuntimeSeconds * 1_000); + + return this.db.transaction(async (tx) => { + const eligible = await tx + .select({ + id: schema.agentDefinitions.id, + allowedRooms: schema.agentDefinitions.allowedRooms, + }) + .from(schema.agentDefinitions) + .innerJoin( + schema.actors, + and( + eq(schema.actors.organisationId, subject.organisationId), + eq(schema.actors.id, schema.agentDefinitions.id), + eq(schema.actors.status, "active"), + ), + ) + .innerJoin( + schema.roomMemberships, + and( + eq(schema.roomMemberships.organisationId, subject.organisationId), + eq(schema.roomMemberships.roomId, input.roomId), + eq(schema.roomMemberships.actorId, schema.agentDefinitions.id), + or( + isNull(schema.roomMemberships.accessExpiresAt), + gt(schema.roomMemberships.accessExpiresAt, new Date()), + ), + ), + ) + .where( + and( + eq(schema.agentDefinitions.organisationId, subject.organisationId), + eq(schema.agentDefinitions.status, "active"), + eq(schema.agentDefinitions.killSwitch, false), + ), + ); + if ( + eligible.length !== 1 || + eligible[0]?.id !== agentId || + !Array.isArray(eligible[0].allowedRooms) || + !eligible[0].allowedRooms.includes(input.roomId) + ) { + throw new Error("Direct-message agent is unavailable"); + } + + const [inserted] = await tx + .insert(schema.agentRuns) + .values({ + id: newId(), + agentId, + organisationId: subject.organisationId, + roomId: input.roomId, + investigationId: source.relatedInvestigationId, + requestedByActorId: subject.actorId, + trigger: "direct_message", + status: "queued", + request: { + kind: "direct_message", + sourceMessageId: input.messageId, + humanRequest: source.plainText, + traceId, + }, + progress: { stage: "queued", percent: 0 }, + deadlineAt, + inputHash, + promptVersion, + runtime, + model, + maximumRuntimeSeconds, + maximumTokenBudget, + maximumCostCents, + idempotencyKey, + }) + .onConflictDoNothing() + .returning(); + const run = + inserted ?? + ( + await tx + .select() + .from(schema.agentRuns) + .where( + and( + eq(schema.agentRuns.organisationId, subject.organisationId), + eq(schema.agentRuns.idempotencyKey, idempotencyKey), + ), + ) + .limit(1) + )[0]; + if ( + !run || + run.agentId !== agentId || + run.roomId !== input.roomId || + run.requestedByActorId !== subject.actorId || + run.inputHash !== inputHash + ) { + throw new Error("Direct-message run idempotency conflict"); + } + if (inserted) { + await tx.insert(schema.agentRunEvents).values({ + id: newId(), + organisationId: subject.organisationId, + runId: run.id, + eventType: "queued", + message: "Direct-message agent run queued", + payload: { + trigger: "direct_message", + sourceMessageId: input.messageId, + roomId: input.roomId, + }, + }); + await appendAuditEvent(tx, { + organisationId: subject.organisationId, + actorId: subject.actorId, + actorType: "human", + action: "agent.run.queued", + targetType: "agent_run", + targetId: run.id, + metadata: { + trigger: "direct_message", + sourceMessageId: input.messageId, + roomId: input.roomId, + }, + traceId: redactObservationText(traceId), + }); + await writeOutbox(tx, { + organisationId: subject.organisationId, + eventType: "agent.run.queued", + aggregateType: "agent_run", + aggregateId: run.id, + queueName: "muster-agents", + payload: { runId: run.id }, + idempotencyKey: `agent.run.queued:${run.id}`, + traceId: redactObservationText(traceId), + }); + } + return { + handled: true, + queued: true, + duplicate: !inserted, + agentId: run.agentId, + agentRunId: run.id, + status: run.status, + }; + }); + } +} From fb8999ef9012ec2eb6411174f8873a7a03170a49 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 13:45:12 +1000 Subject: [PATCH 03/47] fix(research): surface terminal run status --- .env.example | 2 + apps/worker/src/index.ts | 17 +- apps/worker/src/research-config.test.ts | 33 +++ apps/worker/src/research-status.test.ts | 307 +++++++++++++++++++++++ apps/worker/src/research-status.ts | 108 ++++++++ deploy/docker/.env.homelab.example | 2 + deploy/docker/docker-compose.homelab.yml | 2 + docs/operations/alfie-research.md | 2 +- 8 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 apps/worker/src/research-config.test.ts create mode 100644 apps/worker/src/research-status.test.ts create mode 100644 apps/worker/src/research-status.ts diff --git a/.env.example b/.env.example index d401e5b..8f5aca1 100644 --- a/.env.example +++ b/.env.example @@ -30,3 +30,5 @@ SENTINEL_CLIENT_ID= SENTINEL_CLIENT_SECRET= SENTINEL_WORKSPACE_ID= MUSTER_MOCK_INTEGRATIONS=true +# Optional comma-separated HTTPS origins for additional Alfie research feeds. +MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS= diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 217d3b9..7a65ebb 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -47,6 +47,7 @@ import { researchRunIdempotencyKey, staleResearchEvidence, } from "./research-scheduler.ts"; +import { appendResearchTerminalMessage } from "./research-status.ts"; import { queueDueParkerReports } from "./parker-scheduler.ts"; import { processParkerReport } from "./parker-report.ts"; @@ -1648,7 +1649,7 @@ async function processResearchRun( ) .limit(1); if (!run) throw new Error("Authoritative research run not found"); - if (run.run.status === "completed") return; + if (run.run.status === "completed" || run.run.status === "failed") return; if (!strings(run.agent.allowedTools).includes("research.feeds.read")) throw new Error("Alfie feed tool is revoked"); await db.transaction(async (tx) => { @@ -1911,6 +1912,14 @@ async function processResearchRun( eq(schema.agentRuns.id, run.run.agentRunId), ), ); + if (posted === 0) { + await appendResearchTerminalMessage(tx, { + organisationId, + researchRunId, + kind: "no_changes", + traceId, + }); + } await appendAuditEvent(tx, { organisationId, actorId: run.agent.id, @@ -1973,6 +1982,12 @@ async function processResearchRun( eq(schema.agentRuns.id, run.run.agentRunId), ), ); + await appendResearchTerminalMessage(tx, { + organisationId, + researchRunId, + kind: "failed", + traceId, + }); await appendAuditEvent(tx, { organisationId, actorId: run.agent.id, diff --git a/apps/worker/src/research-config.test.ts b/apps/worker/src/research-config.test.ts new file mode 100644 index 0000000..eb31f12 --- /dev/null +++ b/apps/worker/src/research-config.test.ts @@ -0,0 +1,33 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const repositoryRoot = fileURLToPath(new URL("../../../", import.meta.url)); + +function repositoryFile(path: string) { + return readFileSync(new URL(path, `file://${repositoryRoot}/`), "utf8"); +} + +describe("Alfie homelab research configuration", () => { + it("passes an explicitly configured feed-origin allowlist to web and worker only", () => { + const compose = repositoryFile("deploy/docker/docker-compose.homelab.yml"); + expect( + compose.match( + /MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS: \$\{MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS:-\}/g, + ), + ).toHaveLength(2); + }); + + it("documents an empty safe default without enabling a mock origin", () => { + for (const path of [".env.example", "deploy/docker/.env.homelab.example"]) { + const example = repositoryFile(path); + expect(example).toMatch(/^MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS=$/m); + expect(example).not.toMatch( + /^MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS=.*https?:/m, + ); + } + expect(repositoryFile("docs/operations/alfie-research.md")).toContain( + "Homelab Compose passes this value unchanged to web and worker services", + ); + }); +}); diff --git a/apps/worker/src/research-status.test.ts b/apps/worker/src/research-status.test.ts new file mode 100644 index 0000000..8f7a3a1 --- /dev/null +++ b/apps/worker/src/research-status.test.ts @@ -0,0 +1,307 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { closeDatabase, database, newId, schema } from "@muster/database"; +import { and, eq, inArray } from "drizzle-orm"; +import { + appendResearchTerminalMessage, + researchTerminalMessageText, + type ResearchTerminalMessageKind, +} from "./research-status.ts"; + +const describeIntegration = + process.env.MUSTER_INTEGRATION_TESTS === "true" + ? describe.sequential + : describe.skip; + +describe("research terminal status copy", () => { + it("uses fixed redacted copy for terminal failure and no-change success", () => { + expect(researchTerminalMessageText("failed")).toBe( + "Alfie research could not complete after the final retry. No feed content was posted. Review the agent run before retrying.", + ); + expect(researchTerminalMessageText("no_changes")).toBe( + "Alfie research completed. No new or changed findings were found.", + ); + }); +}); + +describeIntegration("research terminal status persistence", () => { + const db = database(); + const watchlistIds: string[] = []; + const researchRunIds: string[] = []; + const agentRunIds: string[] = []; + let organisationId = ""; + let agentId = ""; + let roomId = ""; + let promptVersion = ""; + let runtime = ""; + let model = ""; + + beforeAll(async () => { + const [agent] = await db + .select() + .from(schema.agentDefinitions) + .where( + and( + eq(schema.agentDefinitions.name, "Alfie"), + eq(schema.agentDefinitions.status, "active"), + eq(schema.agentDefinitions.killSwitch, false), + ), + ) + .limit(1); + if ( + !agent || + !Array.isArray(agent.allowedRooms) || + typeof agent.allowedRooms[0] !== "string" + ) { + throw new Error("Bootstrap Alfie with an allowed room before tests"); + } + organisationId = agent.organisationId; + agentId = agent.id; + roomId = agent.allowedRooms[0]; + promptVersion = agent.systemPromptVersion; + runtime = agent.runtime; + model = agent.model; + }); + + async function terminalRun( + kind: ResearchTerminalMessageKind, + canary: string, + posted = 0, + ) { + const watchlistId = newId(); + const researchRunId = newId(); + const agentRunId = newId(); + watchlistIds.push(watchlistId); + researchRunIds.push(researchRunId); + agentRunIds.push(agentRunId); + const terminalStatus = kind === "failed" ? "failed" : "completed"; + const sourceUrl = `https://secret.example/feed?token=${canary}`; + + await db.insert(schema.researchWatchlists).values({ + id: watchlistId, + organisationId, + roomId, + createdByActorId: agentId, + name: `Synthetic terminal visibility ${watchlistId}`, + vendors: [], + technologies: [], + sources: [{ name: canary, url: sourceUrl }], + cadenceMinutes: 240, + enabled: false, + nextRunAt: new Date(Date.now() + 86_400_000), + }); + await db.insert(schema.agentRuns).values({ + id: agentRunId, + agentId, + organisationId, + roomId, + requestedByActorId: agentId, + trigger: "schedule", + status: terminalStatus, + request: { researchRunId, watchlistId, secret: canary }, + progress: { stage: terminalStatus, percent: 100 }, + inputHash: "0".repeat(64), + promptVersion, + runtime, + model, + structuredOutput: + kind === "no_changes" ? { posted, sources: 1, briefHashes: [] } : null, + error: kind === "failed" ? `Bearer ${canary} from ${sourceUrl}` : null, + failureCode: kind === "failed" ? "research_feed_failed" : null, + completedAt: new Date(), + idempotencyKey: `test:research-terminal-agent:${agentRunId}`, + }); + await db.insert(schema.researchRuns).values({ + id: researchRunId, + organisationId, + watchlistId, + agentRunId, + status: terminalStatus, + sourceLimit: 5, + tokenBudget: 1_000, + costLimitCents: 10, + timeLimitSeconds: 60, + idempotencyKey: `test:research-terminal:${researchRunId}`, + completedAt: new Date(), + error: kind === "failed" ? `Bearer ${canary} from ${sourceUrl}` : null, + }); + return { researchRunId, agentRunId, sourceUrl }; + } + + async function append( + researchRunId: string, + kind: ResearchTerminalMessageKind, + scopedOrganisationId = organisationId, + ) { + return db.transaction((tx) => + appendResearchTerminalMessage(tx, { + organisationId: scopedOrganisationId, + researchRunId, + kind, + traceId: `test:research-terminal:${researchRunId}`, + }), + ); + } + + it("appends one scoped redacted failure message and remains replay safe", async () => { + const canary = `synthetic-research-secret-${newId()}`; + const run = await terminalRun("failed", canary); + + expect(await append(run.researchRunId, "failed", newId())).toBeNull(); + const firstMessageId = await append(run.researchRunId, "failed"); + expect(firstMessageId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, + ); + expect(await append(run.researchRunId, "failed")).toBeNull(); + + const messages = await db + .select() + .from(schema.messages) + .where( + and( + eq(schema.messages.organisationId, organisationId), + eq( + schema.messages.idempotencyKey, + `research.status.message:${run.researchRunId}`, + ), + ), + ); + const outbox = await db + .select() + .from(schema.outboxEvents) + .where( + eq( + schema.outboxEvents.idempotencyKey, + `room.message.created:research-status:${run.researchRunId}`, + ), + ); + + expect(messages).toHaveLength(1); + expect(messages[0]).toMatchObject({ + id: firstMessageId, + organisationId, + roomId, + authorActorId: agentId, + messageType: "agent-status", + relatedAgentRunId: run.agentRunId, + }); + expect(messages[0]?.document).toMatchObject({ + type: "research-run-status", + researchRunId: run.researchRunId, + agentRunId: run.agentRunId, + status: "failed", + failureCode: "research_feed_failed", + trust: "agent-status", + }); + expect(outbox).toHaveLength(1); + expect(outbox[0]).toMatchObject({ + organisationId, + eventType: "room.message.created", + aggregateType: "message", + aggregateId: firstMessageId, + payload: { messageId: firstMessageId, roomId }, + }); + const visibleRecord = JSON.stringify({ messages, outbox }); + expect(visibleRecord).not.toContain(canary); + expect(visibleRecord).not.toContain(run.sourceUrl); + expect(visibleRecord).not.toContain("secret.example"); + }); + + it("appends one no-change success message and refuses a run with findings", async () => { + const noChanges = await terminalRun( + "no_changes", + `synthetic-no-change-${newId()}`, + ); + const withFinding = await terminalRun( + "no_changes", + `synthetic-finding-${newId()}`, + 1, + ); + + const firstMessageId = await append(noChanges.researchRunId, "no_changes"); + expect(firstMessageId).toBeTruthy(); + expect(await append(noChanges.researchRunId, "no_changes")).toBeNull(); + expect(await append(withFinding.researchRunId, "no_changes")).toBeNull(); + + const messages = await db + .select() + .from(schema.messages) + .where( + eq( + schema.messages.idempotencyKey, + `research.status.message:${noChanges.researchRunId}`, + ), + ); + const withFindingMessages = await db + .select() + .from(schema.messages) + .where( + eq( + schema.messages.idempotencyKey, + `research.status.message:${withFinding.researchRunId}`, + ), + ); + expect(messages).toHaveLength(1); + expect(messages[0]).toMatchObject({ + plainText: + "Alfie research completed. No new or changed findings were found.", + relatedAgentRunId: noChanges.agentRunId, + }); + expect(messages[0]?.document).toMatchObject({ + status: "completed_no_changes", + researchRunId: noChanges.researchRunId, + }); + expect(withFindingMessages).toHaveLength(0); + }); + + afterAll(async () => { + for (const researchRunId of researchRunIds) { + await db + .delete(schema.outboxEvents) + .where( + eq( + schema.outboxEvents.idempotencyKey, + `room.message.created:research-status:${researchRunId}`, + ), + ); + await db + .delete(schema.messages) + .where( + eq( + schema.messages.idempotencyKey, + `research.status.message:${researchRunId}`, + ), + ); + } + if (researchRunIds.length > 0) { + await db + .delete(schema.researchRuns) + .where( + and( + eq(schema.researchRuns.organisationId, organisationId), + inArray(schema.researchRuns.id, researchRunIds), + ), + ); + } + for (const agentRunId of agentRunIds) { + await db + .delete(schema.agentRuns) + .where( + and( + eq(schema.agentRuns.organisationId, organisationId), + eq(schema.agentRuns.id, agentRunId), + ), + ); + } + for (const watchlistId of watchlistIds) { + await db + .delete(schema.researchWatchlists) + .where( + and( + eq(schema.researchWatchlists.organisationId, organisationId), + eq(schema.researchWatchlists.id, watchlistId), + ), + ); + } + await closeDatabase(); + }); +}); diff --git a/apps/worker/src/research-status.ts b/apps/worker/src/research-status.ts new file mode 100644 index 0000000..d35f06c --- /dev/null +++ b/apps/worker/src/research-status.ts @@ -0,0 +1,108 @@ +import { database, newId, schema, writeOutbox } from "@muster/database"; +import { and, eq } from "drizzle-orm"; + +type Transaction = Parameters< + Parameters["transaction"]>[0] +>[0]; + +export type ResearchTerminalMessageKind = "failed" | "no_changes"; + +export function researchTerminalMessageText(kind: ResearchTerminalMessageKind) { + return kind === "failed" + ? "Alfie research could not complete after the final retry. No feed content was posted. Review the agent run before retrying." + : "Alfie research completed. No new or changed findings were found."; +} + +export async function appendResearchTerminalMessage( + tx: Transaction, + input: { + organisationId: string; + researchRunId: string; + kind: ResearchTerminalMessageKind; + traceId: string; + }, +) { + const expectedStatus = input.kind === "failed" ? "failed" : "completed"; + const [run] = await tx + .select({ + agentRunId: schema.researchRuns.agentRunId, + agentId: schema.agentRuns.agentId, + roomId: schema.researchWatchlists.roomId, + structuredOutput: schema.agentRuns.structuredOutput, + }) + .from(schema.researchRuns) + .innerJoin( + schema.researchWatchlists, + and( + eq(schema.researchWatchlists.organisationId, input.organisationId), + eq(schema.researchWatchlists.id, schema.researchRuns.watchlistId), + ), + ) + .innerJoin( + schema.agentRuns, + and( + eq(schema.agentRuns.organisationId, input.organisationId), + eq(schema.agentRuns.id, schema.researchRuns.agentRunId), + eq(schema.agentRuns.status, expectedStatus), + ), + ) + .where( + and( + eq(schema.researchRuns.organisationId, input.organisationId), + eq(schema.researchRuns.id, input.researchRunId), + eq(schema.researchRuns.status, expectedStatus), + ), + ) + .limit(1); + if (!run) return null; + + if (input.kind === "no_changes") { + const output = + run.structuredOutput !== null && + typeof run.structuredOutput === "object" && + !Array.isArray(run.structuredOutput) + ? (run.structuredOutput as Record) + : null; + if (output?.posted !== 0) return null; + } + + const messageId = newId(); + const [message] = await tx + .insert(schema.messages) + .values({ + id: messageId, + organisationId: input.organisationId, + roomId: run.roomId, + authorActorId: run.agentId, + messageType: "agent-status", + document: { + type: "research-run-status", + researchRunId: input.researchRunId, + agentRunId: run.agentRunId, + status: input.kind === "failed" ? "failed" : "completed_no_changes", + ...(input.kind === "failed" + ? { failureCode: "research_feed_failed" } + : {}), + trust: "agent-status", + }, + plainText: researchTerminalMessageText(input.kind), + dataClassification: "internal", + relatedAgentRunId: run.agentRunId, + idempotencyKey: `research.status.message:${input.researchRunId}`, + }) + .onConflictDoNothing() + .returning({ id: schema.messages.id }); + if (!message) return null; + + await writeOutbox(tx, { + organisationId: input.organisationId, + eventType: "room.message.created", + aggregateType: "message", + aggregateId: message.id, + queueName: "muster-outbox", + payload: { messageId: message.id, roomId: run.roomId }, + idempotencyKey: `room.message.created:research-status:${input.researchRunId}`, + traceId: input.traceId, + }); + return message.id; +} diff --git a/deploy/docker/.env.homelab.example b/deploy/docker/.env.homelab.example index 684bdb4..e273fbf 100644 --- a/deploy/docker/.env.homelab.example +++ b/deploy/docker/.env.homelab.example @@ -14,3 +14,5 @@ CONNECTOR_ENCRYPTION_KEY=generate-connector-encryption-key KELPIE_API_TOKEN=mock-kelpie-token TAWNY_API_TOKEN=mock-tawny-token BOWER_API_TOKEN=mock-bower-token +# Optional comma-separated HTTPS origins for additional Alfie research feeds. +MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS= diff --git a/deploy/docker/docker-compose.homelab.yml b/deploy/docker/docker-compose.homelab.yml index 6310d20..cf06a12 100644 --- a/deploy/docker/docker-compose.homelab.yml +++ b/deploy/docker/docker-compose.homelab.yml @@ -108,6 +108,7 @@ services: environment: <<: *muster-environment MUSTER_HEALTHCHECK_URL: http://localhost:3000/api/v1/health + MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS: ${MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS:-} ports: - "${MUSTER_HTTP_PORT:-3004}:3000" depends_on: @@ -134,6 +135,7 @@ services: environment: <<: *muster-environment MUSTER_HEALTHCHECK_URL: http://localhost:3001/ready + MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS: ${MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS:-} command: ["/app/worker/dist/index.js"] depends_on: web: diff --git a/docs/operations/alfie-research.md b/docs/operations/alfie-research.md index de0cbcb..8e86071 100644 --- a/docs/operations/alfie-research.md +++ b/docs/operations/alfie-research.md @@ -1,6 +1,6 @@ # Alfie governed research -Alfie reads only allowlisted HTTPS feeds. CISA KEV is built in. Additional origins require `MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS` as a comma-separated origin allowlist before an administrator creates a watchlist. +Alfie reads only allowlisted HTTPS feeds. CISA KEV is built in. Additional origins require `MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS` as a comma-separated origin allowlist before an administrator creates a watchlist. Homelab Compose passes this value unchanged to web and worker services; leave it empty when no additional feed origin is approved. `POST /api/v1/research-watchlists` requires `agents.manage`. A watchlist has vendor/technology terms, a room, a 15-minute to 7-day cadence, and bounded source count. PostgreSQL stores watchlists, runs, source-backed briefs, feedback, and audit events; BullMQ only carries durable identifiers. From 9710db2b7c1ac3ee2125beba6cd78bd81db85452 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 14:12:24 +1000 Subject: [PATCH 04/47] feat(maintenance): add synthetic cleanup manifest guard --- docs/synthetic-cleanup.md | 9 ++ packages/database/package.json | 2 + packages/database/src/index.ts | 1 + .../database/src/synthetic-cleanup.test.ts | 37 +++++ packages/database/src/synthetic-cleanup.ts | 149 ++++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 docs/synthetic-cleanup.md create mode 100644 packages/database/src/synthetic-cleanup.test.ts create mode 100644 packages/database/src/synthetic-cleanup.ts diff --git a/docs/synthetic-cleanup.md b/docs/synthetic-cleanup.md new file mode 100644 index 0000000..e2152d4 --- /dev/null +++ b/docs/synthetic-cleanup.md @@ -0,0 +1,9 @@ +# Synthetic cleanup maintenance + +`pnpm --filter @muster/database cleanup:synthetic` is intentionally not a selector or broad deletion tool. Supply an independently reviewed immutable JSON manifest only through the maintenance runner. + +The manifest contains one organisation, explicit UUID candidates, table digests, and a SHA-256 digest over every field except `digest`. The runner rejects digest changes and the four protected genuine direct-message IDs. + +It uses one serializable transaction. It archives rooms, writes append-only message deletion revisions before hiding proof messages, transitions non-held evidence to `retired` without removing hashes/provenance, rejects selected agent memories, disables selected watchlists, then emits exactly one cleanup audit and outbox event. Audit/outbox rows, evidence metadata, and message history are never deleted. + +Object removal needs separate storage evidence after legal-hold and object-lock review; it is deliberately outside this command. Generate candidates from live state, restore-test exact manifest in an isolated PostgreSQL instance, then run with an operator-approved maintenance actor and trace ID. diff --git a/packages/database/package.json b/packages/database/package.json index 9b67978..02e5aa4 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -29,6 +29,8 @@ "migrate": "tsx src/migrate.ts", "bootstrap": "tsx src/bootstrap.ts", "verify-clean": "tsx src/verify-clean-install.ts", + "cleanup:synthetic": "tsx src/synthetic-cleanup.ts", + "verify:audit": "tsx src/verify-audit-integrity.ts", "seed": "tsx src/seed.ts" }, "dependencies": { diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index 6a3bd32..fd85b3f 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -26,3 +26,4 @@ export * from "./ids.ts"; export * from "./domain-transaction.ts"; export * from "./outbox.ts"; export * from "./repository.ts"; +export * from "./synthetic-cleanup.ts"; diff --git a/packages/database/src/synthetic-cleanup.test.ts b/packages/database/src/synthetic-cleanup.test.ts new file mode 100644 index 0000000..58423df --- /dev/null +++ b/packages/database/src/synthetic-cleanup.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { + parseSyntheticCleanupManifest, + protectedDirectMessageIds, + syntheticCleanupManifestDigest, + type SyntheticCleanupManifest, +} from "./synthetic-cleanup.ts"; + +const unsigned: Omit = { + version: 1, + manifestId: "019fa210-0000-7000-8000-000000000001", + organisationId: "019fa210-0000-7000-8000-000000000002", + maintenanceActorId: "019fa210-0000-7000-8000-000000000003", + generatedAt: "2026-07-27T00:00:00.000Z", + archiveRoomIds: [], hideMessageIds: [], retireEvidenceIds: [], + rejectAgentMemoryIds: [], disableWatchlistIds: [], tableDigests: {}, +}; + +function manifest(overrides: Partial = {}) { + const value = { ...unsigned, ...overrides } as Omit; + return { ...value, digest: syntheticCleanupManifestDigest(value) }; +} + +describe("synthetic cleanup manifest", () => { + it("accepts exact, digest-bound candidates", () => { + expect(parseSyntheticCleanupManifest(manifest())).toMatchObject(unsigned); + }); + + it("rejects tampering", () => { + expect(() => parseSyntheticCleanupManifest({ ...manifest(), archiveRoomIds: ["019fa210-0000-7000-8000-000000000004"] })).toThrow("digest mismatch"); + }); + + it("preserves genuine direct messages", () => { + const value = manifest({ hideMessageIds: [protectedDirectMessageIds[0]] }); + expect(() => parseSyntheticCleanupManifest(value)).toThrow("protected direct message"); + }); +}); diff --git a/packages/database/src/synthetic-cleanup.ts b/packages/database/src/synthetic-cleanup.ts new file mode 100644 index 0000000..932ac7f --- /dev/null +++ b/packages/database/src/synthetic-cleanup.ts @@ -0,0 +1,149 @@ +import { createHash } from "node:crypto"; +import { and, eq, inArray } from "drizzle-orm"; +import { z } from "zod"; +import { appendAuditEvent } from "./domain-transaction.ts"; +import { newId } from "./ids.ts"; +import { database } from "./index.ts"; +import { writeOutbox } from "./outbox.ts"; +import * as schema from "./schema.ts"; + +export const protectedDirectMessageIds = [ + "019fa05a-fff0-76ce-9084-bf0707206d15", + "019fa05b-c62c-7368-8166-a23b68e3057f", + "019fa19f-335e-708e-9ce3-be4083921691", + "019fa19f-5c96-7402-8784-0324bb98d48c", +] as const; + +const ids = z.array(z.uuid()).max(10_000).refine( + (value) => new Set(value).size === value.length, + "Candidate IDs must be unique", +); + +export const SyntheticCleanupManifestSchema = z.object({ + version: z.literal(1), + manifestId: z.uuid(), + organisationId: z.uuid(), + maintenanceActorId: z.uuid(), + generatedAt: z.string().datetime({ offset: true }), + digest: z.string().regex(/^[a-f0-9]{64}$/), + archiveRoomIds: ids.default([]), + hideMessageIds: ids.default([]), + retireEvidenceIds: ids.default([]), + rejectAgentMemoryIds: ids.default([]), + disableWatchlistIds: ids.default([]), + tableDigests: z.record(z.string(), z.string().regex(/^[a-f0-9]{64}$/)), +}).strict(); + +export type SyntheticCleanupManifest = z.infer; + +function canonical(value: unknown): string { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; + return `{${Object.entries(value as Record) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, entry]) => `${JSON.stringify(key)}:${canonical(entry)}`) + .join(",")}}`; +} + +export function syntheticCleanupManifestDigest( + manifest: Omit, +) { + return createHash("sha256").update(canonical(manifest)).digest("hex"); +} + +export function parseSyntheticCleanupManifest(input: unknown) { + const manifest = SyntheticCleanupManifestSchema.parse(input); + const { digest, ...unsigned } = manifest; + if (syntheticCleanupManifestDigest(unsigned) !== digest) { + throw new Error("Cleanup manifest digest mismatch"); + } + if ( + manifest.hideMessageIds.some((id) => + (protectedDirectMessageIds as readonly string[]).includes(id), + ) + ) { + throw new Error("Cleanup manifest includes protected direct message"); + } + return manifest; +} + +/** + * Applies only reversible/governed transitions. Candidate selection and any + * physical object disposition stay outside this command; audit/outbox/message + * history remain immutable. + */ +export async function applySyntheticCleanup( + input: unknown, + traceId: string, + db = database(), +) { + const manifest = parseSyntheticCleanupManifest(input); + return db.transaction(async (tx) => { + const idempotencyKey = `maintenance.synthetic-cleanup:${manifest.manifestId}`; + const [prior] = await tx + .select({ id: schema.outboxEvents.id }) + .from(schema.outboxEvents) + .where(eq(schema.outboxEvents.idempotencyKey, idempotencyKey)) + .limit(1) + .for("update"); + if (prior) return { applied: false, manifestId: manifest.manifestId }; + + const now = new Date(); + if (manifest.archiveRoomIds.length) { + await tx.update(schema.rooms).set({ archivedAt: now, updatedAt: now }).where(and( + eq(schema.rooms.organisationId, manifest.organisationId), + inArray(schema.rooms.id, manifest.archiveRoomIds), + )); + } + for (const messageId of manifest.hideMessageIds) { + const [message] = await tx.select().from(schema.messages).where(and( + eq(schema.messages.organisationId, manifest.organisationId), + eq(schema.messages.id, messageId), + )).limit(1).for("update"); + if (!message || message.deletedAt) continue; + await tx.insert(schema.messageRevisions).values({ + id: newId(), organisationId: manifest.organisationId, messageId, + actorId: manifest.maintenanceActorId, revisionType: "delete", + previousDocument: message.document, previousPlainText: message.plainText, + nextDocument: null, nextPlainText: null, + reason: `Synthetic cleanup manifest ${manifest.manifestId}`, + idempotencyKey: `${idempotencyKey}:message:${messageId}`, + }); + await tx.update(schema.messages).set({ + document: { type: "doc", content: [] }, plainText: "Message deleted", deletedAt: now, + }).where(and(eq(schema.messages.organisationId, manifest.organisationId), eq(schema.messages.id, messageId))); + } + if (manifest.retireEvidenceIds.length) { + await tx.update(schema.evidence).set({ retentionState: "retired" }).where(and( + eq(schema.evidence.organisationId, manifest.organisationId), + inArray(schema.evidence.id, manifest.retireEvidenceIds), + eq(schema.evidence.legalHold, false), + )); + } + if (manifest.rejectAgentMemoryIds.length) { + await tx.update(schema.agentMemories).set({ status: "rejected", expiresAt: now }).where(and( + eq(schema.agentMemories.organisationId, manifest.organisationId), + inArray(schema.agentMemories.id, manifest.rejectAgentMemoryIds), + )); + } + if (manifest.disableWatchlistIds.length) { + await tx.update(schema.researchWatchlists).set({ enabled: false, updatedAt: now }).where(and( + eq(schema.researchWatchlists.organisationId, manifest.organisationId), + inArray(schema.researchWatchlists.id, manifest.disableWatchlistIds), + )); + } + await appendAuditEvent(tx, { + organisationId: manifest.organisationId, actorId: manifest.maintenanceActorId, + actorType: "human", action: "maintenance.synthetic_cleanup.applied", + targetType: "cleanup_manifest", targetId: manifest.manifestId, + metadata: { digest: manifest.digest, tableDigests: manifest.tableDigests }, traceId, + }); + await writeOutbox(tx, { + organisationId: manifest.organisationId, eventType: "maintenance.synthetic_cleanup.applied", + aggregateType: "cleanup_manifest", aggregateId: manifest.manifestId, + queueName: "muster-outbox", payload: { manifestId: manifest.manifestId, digest: manifest.digest }, + idempotencyKey, traceId, + }); + return { applied: true, manifestId: manifest.manifestId }; + }, { isolationLevel: "serializable", accessMode: "read write" }); +} From 161b0b78eb826c2ce574fa75f126be374ff6ded7 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 14:12:25 +1000 Subject: [PATCH 05/47] fix(agents): close DM delivery and gateway gaps --- .env.example | 1 + README.md | 6 +- apps/agent-gateway/src/index.ts | 44 +++- .../src/runtime.integration.test.ts | 173 +++++++++++++++- apps/agent-gateway/src/runtime.ts | 189 ++++++++++++++++-- apps/agent-gateway/src/service-auth.test.ts | 30 +++ apps/agent-gateway/src/service-auth.ts | 21 ++ apps/web/app/api/v1/agent-runs/[id]/route.ts | 6 +- .../app/api/v1/rooms/[id]/messages/route.ts | 64 +++--- .../web/app/api/v1/tasks/[id]/cancel/route.ts | 2 + .../web/app/api/v1/tasks/[id]/events/route.ts | 6 +- apps/web/lib/agent-gateway.ts | 8 + .../agent-learning-domain.integration.test.ts | 124 ++++++++++-- apps/web/lib/agent-learning-domain.ts | 97 ++++++++- apps/worker/src/index.ts | 3 + deploy/docker/.env.homelab.example | 1 + deploy/docker/README.md | 10 +- deploy/docker/docker-compose.homelab.yml | 1 + docker-compose.yml | 1 + docs/synthetic-cleanup.md | 2 +- packages/database/src/synthetic-cleanup.ts | 17 +- playwright.config.ts | 6 +- scripts/install-homelab.sh | 8 + scripts/test-install-homelab.sh | 7 + 24 files changed, 736 insertions(+), 91 deletions(-) create mode 100644 apps/agent-gateway/src/service-auth.test.ts create mode 100644 apps/agent-gateway/src/service-auth.ts create mode 100644 apps/web/lib/agent-gateway.ts diff --git a/.env.example b/.env.example index 8f5aca1..acbe41f 100644 --- a/.env.example +++ b/.env.example @@ -30,5 +30,6 @@ SENTINEL_CLIENT_ID= SENTINEL_CLIENT_SECRET= SENTINEL_WORKSPACE_ID= MUSTER_MOCK_INTEGRATIONS=true +MUSTER_AGENT_GATEWAY_TOKEN=replace-with-at-least-32-random-bytes # Optional comma-separated HTTPS origins for additional Alfie research feeds. MUSTER_RESEARCH_ALLOWED_FEED_ORIGINS= diff --git a/README.md b/README.md index 227752e..7155219 100644 --- a/README.md +++ b/README.md @@ -177,8 +177,10 @@ that will use the service. When using HTTPS behind a reverse proxy, set `AUTH_SECURE_COOKIES=true`; do not add broad wildcard origins. The installer creates `.env.homelab` with mode `600`. Keep it out of source -control. The generated topology still uses synthetic connector endpoints until -you deliberately configure governed real connectors. +control. Its generated `MUSTER_AGENT_GATEWAY_TOKEN` authenticates internal +web/worker calls to the unexposed agent gateway; do not reuse or publish it. +The generated topology still uses synthetic connector endpoints until you +deliberately configure governed real connectors. ### Codex subscription authentication diff --git a/apps/agent-gateway/src/index.ts b/apps/agent-gateway/src/index.ts index 9762b29..3a5778b 100644 --- a/apps/agent-gateway/src/index.ts +++ b/apps/agent-gateway/src/index.ts @@ -15,6 +15,10 @@ import { import { and, eq } from "drizzle-orm"; import { z } from "zod"; import { DurableAgentRuntime } from "./runtime.ts"; +import { + isGatewayRequestAuthorised, + parseGatewayOrganisationId, +} from "./service-auth.ts"; const AgentRunRequestSchema = AgentInvestigationJobSchema.extend({ humanRequest: z.string().trim().min(1).max(4_000).optional(), @@ -24,6 +28,10 @@ const executionRuntime = process.env.MUSTER_AGENT_RUNTIME === "mock" ? "mock" : "codex"; const codexHome = process.env.CODEX_HOME ?? "/var/lib/muster/codex"; const globalKillSwitch = process.env.AGENT_KILL_SWITCH === "true"; +const gatewayToken = z + .string() + .min(32) + .parse(process.env.MUSTER_AGENT_GATEWAY_TOKEN); const runtime = new DurableAgentRuntime({ executionRuntime, codexHome, @@ -47,6 +55,12 @@ async function body(request: IncomingMessage): Promise { return JSON.parse(Buffer.concat(chunks).toString("utf8")); } +function requestOrganisationId(request: IncomingMessage) { + return parseGatewayOrganisationId( + request.headers["x-muster-organisation-id"], + ); +} + async function queueDirectRun( input: z.infer, idempotencyKey: string, @@ -170,12 +184,26 @@ const server = createServer(async (incoming, response) => { return; } + if ( + !isGatewayRequestAuthorised(incoming.headers.authorization, gatewayToken) + ) { + response.writeHead(401); + response.end(JSON.stringify({ error: "Unauthorised" })); + return; + } + const runMatch = incoming.method === "GET" ? url.pathname.match(/^\/v1\/runs\/([^/]+)$/) : null; if (runMatch?.[1]) { - const run = await runtime.read(runMatch[1]); + const organisationId = requestOrganisationId(incoming); + if (!organisationId) { + response.writeHead(400); + response.end(JSON.stringify({ error: "Organisation header required" })); + return; + } + const run = await runtime.read(runMatch[1], organisationId); response.writeHead(run ? 200 : 404); response.end(JSON.stringify(run ?? { error: "Run not found" })); return; @@ -234,6 +262,12 @@ const server = createServer(async (incoming, response) => { ); return; } + const organisationId = requestOrganisationId(incoming); + if (!organisationId || organisationId !== parsed.data.organisationId) { + response.writeHead(403); + response.end(JSON.stringify({ error: "Organisation mismatch" })); + return; + } const idempotencyKey = incoming.headers["idempotency-key"]?.toString().trim() || `api:${parsed.data.traceId}`; @@ -269,7 +303,13 @@ const server = createServer(async (incoming, response) => { ? url.pathname.match(/^\/v1\/runs\/([^/]+)\/cancel$/) : null; if (cancelMatch?.[1]) { - const cancelled = await runtime.cancel(cancelMatch[1]); + const organisationId = requestOrganisationId(incoming); + if (!organisationId) { + response.writeHead(400); + response.end(JSON.stringify({ error: "Organisation header required" })); + return; + } + const cancelled = await runtime.cancel(cancelMatch[1], organisationId); response.writeHead(cancelled ? 202 : 404); response.end( JSON.stringify({ diff --git a/apps/agent-gateway/src/runtime.integration.test.ts b/apps/agent-gateway/src/runtime.integration.test.ts index 8d0237e..8a4766b 100644 --- a/apps/agent-gateway/src/runtime.integration.test.ts +++ b/apps/agent-gateway/src/runtime.integration.test.ts @@ -262,7 +262,11 @@ describeIntegration("durable agent runtime", () => { codexHome: "/tmp/muster-runtime-integration", }); expect( - await runtime.cancel(run.id, "Synthetic operator cancellation"), + await runtime.cancel( + run.id, + organisationId, + "Synthetic operator cancellation", + ), ).toBe(true); const cancelled = await waitFor(run.id, "cancelled"); expect(cancelled.cancellationRequestedAt).not.toBeNull(); @@ -271,6 +275,22 @@ describeIntegration("durable agent runtime", () => { ); }); + it("scopes run reads and cancellations by organisation", async () => { + const run = await insertRun("cross-organisation-guard"); + const runtime = new DurableAgentRuntime({ + executionRuntime: "mock", + codexHome: "/tmp/muster-runtime-integration", + }); + const otherOrganisationId = newId(); + + await expect(runtime.read(run.id, otherOrganisationId)).resolves.toBeNull(); + await expect(runtime.cancel(run.id, otherOrganisationId)).resolves.toBe( + false, + ); + const persisted = await waitFor(run.id, "queued"); + expect(persisted.organisationId).toBe(organisationId); + }); + it("returns a redacted observer projection without changing the execution record", async () => { const canary = `synthetic-api-secret-${newId()}`; const run = await insertRun("redacted-observer", { @@ -302,7 +322,7 @@ describeIntegration("durable agent runtime", () => { codexHome: "/tmp/muster-runtime-integration", }); - const projection = await runtime.read(run.id); + const projection = await runtime.read(run.id, organisationId); const serialisedProjection = JSON.stringify(projection); expect(serialisedProjection).not.toContain(canary); expect(serialisedProjection).toContain("[REDACTED]"); @@ -631,7 +651,7 @@ describeIntegration("durable agent runtime", () => { await waitFor(run.id, "completed"); runtime.stop(); - const [reply, outbox] = await Promise.all([ + const [replies, outbox] = await Promise.all([ database() .select() .from(schema.messages) @@ -640,8 +660,7 @@ describeIntegration("durable agent runtime", () => { schema.messages.idempotencyKey, `agent-direct-message-reply:${run.id}`, ), - ) - .then((rows) => rows[0]), + ), database() .select() .from(schema.outboxEvents) @@ -650,9 +669,11 @@ describeIntegration("durable agent runtime", () => { schema.outboxEvents.idempotencyKey, `room.message.created:agent-direct-message:${run.id}`, ), - ) - .then((rows) => rows[0]), + ), ]); + expect(replies).toHaveLength(1); + expect(outbox).toHaveLength(1); + const reply = replies[0]; expect(reply).toMatchObject({ roomId: source.roomId, threadParentId: source.messageId, @@ -667,7 +688,7 @@ describeIntegration("durable agent runtime", () => { agentRunId: run.id, trust: "agent-analysis", }); - expect(outbox?.aggregateId).toBe(reply?.id); + expect(outbox[0]?.aggregateId).toBe(reply?.id); }); it("projects a failed direct-message run as one linked room reply", async () => { @@ -779,4 +800,140 @@ describeIntegration("durable agent runtime", () => { agentRunId: run.id, }); }); + + it("projects a cancelled direct-message run exactly once", async () => { + const source = await directMessageSource("cancelled"); + const run = await insertRun("direct-cancelled", { + roomId: source.roomId, + request: { + kind: "direct_message", + sourceMessageId: source.messageId, + humanRequest: "Review the synthetic direct request", + traceId: `integration-direct-${source.messageId}`, + }, + }); + const runtime = new DurableAgentRuntime({ + executionRuntime: "mock", + codexHome: "/tmp/muster-runtime-integration", + mockDelayMs: 10, + }); + expect( + await runtime.cancel( + run.id, + organisationId, + "Synthetic operator cancellation", + ), + ).toBe(true); + await waitFor(run.id, "cancelled"); + runtime.stop(); + + const [replies, outbox] = await Promise.all([ + database() + .select() + .from(schema.messages) + .where( + eq( + schema.messages.idempotencyKey, + `agent-direct-message-reply:${run.id}`, + ), + ), + database() + .select() + .from(schema.outboxEvents) + .where( + eq( + schema.outboxEvents.idempotencyKey, + `room.message.created:agent-direct-message:${run.id}`, + ), + ), + ]); + expect(replies).toHaveLength(1); + expect(replies[0]).toMatchObject({ + roomId: source.roomId, + threadParentId: source.messageId, + authorActorId: agentId, + messageType: "agent-status", + relatedAgentRunId: run.id, + }); + expect(replies[0]?.document).toMatchObject({ + status: "cancelled", + failureCode: "operator_cancelled", + sourceMessageId: source.messageId, + agentRunId: run.id, + }); + expect(outbox).toHaveLength(1); + }); + + it("fails queued direct messages when room authorisation is revoked", async () => { + const source = await directMessageSource("revoked-room"); + const run = await insertRun("direct-revoked-room", { + roomId: source.roomId, + request: { + kind: "direct_message", + sourceMessageId: source.messageId, + humanRequest: "Review the synthetic direct request", + traceId: `integration-direct-${source.messageId}`, + }, + }); + const [definition] = await database() + .select({ allowedRooms: schema.agentDefinitions.allowedRooms }) + .from(schema.agentDefinitions) + .where(eq(schema.agentDefinitions.id, agentId)) + .limit(1); + await database() + .update(schema.agentDefinitions) + .set({ allowedRooms: [] }) + .where(eq(schema.agentDefinitions.id, agentId)); + const runtime = new DurableAgentRuntime({ + executionRuntime: "mock", + codexHome: "/tmp/muster-runtime-integration", + mockDelayMs: 10, + }); + try { + await runtime.dispatch(); + const failed = await waitFor(run.id, "failed"); + expect(failed.failureCode).toBe("direct_message_not_authorised"); + expect(failed.startedAt).toBeNull(); + } finally { + runtime.stop(); + await database() + .update(schema.agentDefinitions) + .set({ allowedRooms: definition?.allowedRooms ?? [] }) + .where(eq(schema.agentDefinitions.id, agentId)); + } + }); + + it("reports inactive agents truthfully before execution", async () => { + const source = await directMessageSource("inactive"); + const run = await insertRun("direct-inactive", { + roomId: source.roomId, + request: { + kind: "direct_message", + sourceMessageId: source.messageId, + humanRequest: "Review the synthetic direct request", + traceId: `integration-direct-${source.messageId}`, + }, + }); + await database() + .update(schema.agentDefinitions) + .set({ status: "inactive" }) + .where(eq(schema.agentDefinitions.id, agentId)); + const runtime = new DurableAgentRuntime({ + executionRuntime: "mock", + codexHome: "/tmp/muster-runtime-integration", + mockDelayMs: 10, + }); + try { + await runtime.dispatch(); + const failed = await waitFor(run.id, "failed"); + expect(failed.failureCode).toBe("agent_inactive"); + expect(failed.startedAt).toBeNull(); + } finally { + runtime.stop(); + await database() + .update(schema.agentDefinitions) + .set({ status: "active" }) + .where(eq(schema.agentDefinitions.id, agentId)); + } + }); }); diff --git a/apps/agent-gateway/src/runtime.ts b/apps/agent-gateway/src/runtime.ts index 318019a..86f287b 100644 --- a/apps/agent-gateway/src/runtime.ts +++ b/apps/agent-gateway/src/runtime.ts @@ -27,7 +27,7 @@ import { TenantRepository, writeOutbox, } from "@muster/database"; -import { and, asc, eq, inArray, isNull, lt, or, sql } from "drizzle-orm"; +import { and, asc, eq, gt, inArray, isNull, lt, or, sql } from "drizzle-orm"; import { z } from "zod"; type AgentRunRow = typeof schema.agentRuns.$inferSelect; @@ -71,6 +71,11 @@ async function projectDirectMessageTerminalReply( status: "failed"; failureCode: string; error: string; + } + | { + status: "cancelled"; + failureCode: string; + error: string; }, ) { if ( @@ -82,11 +87,33 @@ async function projectDirectMessageTerminalReply( const [source] = await tx .select({ id: schema.messages.id }) .from(schema.messages) + .innerJoin( + schema.rooms, + and( + eq(schema.rooms.organisationId, run.organisationId), + eq(schema.rooms.id, run.roomId), + eq(schema.rooms.roomType, "direct"), + isNull(schema.rooms.archivedAt), + ), + ) + .innerJoin( + schema.roomMemberships, + and( + eq(schema.roomMemberships.organisationId, run.organisationId), + eq(schema.roomMemberships.roomId, run.roomId), + eq(schema.roomMemberships.actorId, run.agentId), + or( + isNull(schema.roomMemberships.accessExpiresAt), + gt(schema.roomMemberships.accessExpiresAt, new Date()), + ), + ), + ) .where( and( eq(schema.messages.organisationId, run.organisationId), eq(schema.messages.id, request.sourceMessageId), eq(schema.messages.roomId, run.roomId), + isNull(schema.messages.deletedAt), ), ) .limit(1); @@ -95,7 +122,9 @@ async function projectDirectMessageTerminalReply( const completed = terminal.status === "completed"; const plainText = completed ? terminalSummary(terminal.output) - : `The agent could not complete this request (${terminal.failureCode}). ${redactObservationText(terminal.error, { maxStringLength: 2_000 })}`; + : terminal.status === "cancelled" + ? `The agent request was cancelled (${terminal.failureCode}).` + : `The agent could not complete this request (${terminal.failureCode}). Retry the request or contact an operator if the problem continues.`; const messageId = newId(); const [message] = await tx .insert(schema.messages) @@ -428,12 +457,17 @@ export class DurableAgentRuntime { this.lastReadinessSnapshotAt = now.getTime(); } - async read(runId: string) { + async read(runId: string, organisationId: string) { const db = database(); const [run] = await db .select() .from(schema.agentRuns) - .where(eq(schema.agentRuns.id, runId)) + .where( + and( + eq(schema.agentRuns.organisationId, organisationId), + eq(schema.agentRuns.id, runId), + ), + ) .limit(1); if (!run) return null; const events = await db @@ -469,7 +503,11 @@ export class DurableAgentRuntime { return redactForObservation(projection) as typeof projection; } - async cancel(runId: string, reason = "Cancelled by operator") { + async cancel( + runId: string, + organisationId: string, + reason = "Cancelled by operator", + ) { const now = new Date(); const [run] = await database().transaction(async (tx) => { const [updated] = await tx @@ -485,6 +523,7 @@ export class DurableAgentRuntime { }) .where( and( + eq(schema.agentRuns.organisationId, organisationId), eq(schema.agentRuns.id, runId), or( eq(schema.agentRuns.status, "awaiting_approval"), @@ -516,6 +555,16 @@ export class DurableAgentRuntime { this.request(updated).traceId ?? `agent-run-${updated.id}`, ), }); + await projectDirectMessageTerminalReply( + tx, + updated, + this.request(updated), + { + status: "cancelled", + failureCode: "operator_cancelled", + error: reason, + }, + ); const [hunt] = await tx .update(schema.huntRuns) .set({ @@ -617,8 +666,17 @@ export class DurableAgentRuntime { .select({ status: schema.agentDefinitions.status, killSwitch: schema.agentDefinitions.killSwitch, + allowedRooms: schema.agentDefinitions.allowedRooms, + actorStatus: schema.actors.status, }) .from(schema.agentDefinitions) + .leftJoin( + schema.actors, + and( + eq(schema.actors.organisationId, candidate.organisationId), + eq(schema.actors.id, schema.agentDefinitions.id), + ), + ) .where( and( eq(schema.agentDefinitions.id, candidate.agentId), @@ -629,18 +687,92 @@ export class DurableAgentRuntime { ), ) .limit(1); - if ( - !definition || + const request = this.request(candidate); + let eligibilityFailure: { code: string; message: string } | undefined; + if (!definition) { + eligibilityFailure = { + code: "agent_unavailable", + message: "Agent definition is unavailable", + }; + } else if (definition.killSwitch) { + eligibilityFailure = { + code: "agent_kill_switch", + message: "Agent is disabled by its kill switch", + }; + } else if ( definition.status !== "active" || - definition.killSwitch + definition.actorStatus !== "active" ) { + eligibilityFailure = { + code: "agent_inactive", + message: "Agent is inactive", + }; + } else if (request.kind === "direct_message") { + const sourceMessageId = request.sourceMessageId; + const roomId = candidate.roomId; + if ( + !sourceMessageId || + !roomId || + !Array.isArray(definition.allowedRooms) || + !definition.allowedRooms.includes(roomId) + ) { + eligibilityFailure = { + code: "direct_message_not_authorised", + message: "Direct-message room is no longer authorised", + }; + } else { + const [authorisedRoom] = await tx + .select({ id: schema.messages.id }) + .from(schema.messages) + .innerJoin( + schema.rooms, + and( + eq(schema.rooms.organisationId, candidate.organisationId), + eq(schema.rooms.id, roomId), + eq(schema.rooms.roomType, "direct"), + isNull(schema.rooms.archivedAt), + ), + ) + .innerJoin( + schema.roomMemberships, + and( + eq( + schema.roomMemberships.organisationId, + candidate.organisationId, + ), + eq(schema.roomMemberships.roomId, roomId), + eq(schema.roomMemberships.actorId, candidate.agentId), + or( + isNull(schema.roomMemberships.accessExpiresAt), + gt(schema.roomMemberships.accessExpiresAt, now), + ), + ), + ) + .where( + and( + eq(schema.messages.organisationId, candidate.organisationId), + eq(schema.messages.id, sourceMessageId), + eq(schema.messages.roomId, roomId), + isNull(schema.messages.deletedAt), + ), + ) + .limit(1); + if (!authorisedRoom) { + eligibilityFailure = { + code: "direct_message_not_authorised", + message: "Direct-message room is no longer authorised", + }; + } + } + } + if (eligibilityFailure) { const [disabled] = await tx .update(schema.agentRuns) .set({ status: "failed", completedAt: now, - failureCode: "agent_kill_switch", - error: "Agent is disabled by its kill switch", + failureCode: eligibilityFailure.code, + error: eligibilityFailure.message, leaseExpiresAt: null, }) .where( @@ -656,8 +788,8 @@ export class DurableAgentRuntime { organisationId: disabled.organisationId, runId: disabled.id, eventType: "failed", - message: "Agent kill switch blocked execution", - payload: { failureCode: "agent_kill_switch" }, + message: eligibilityFailure.message, + payload: { failureCode: eligibilityFailure.code }, }); await appendAuditEvent(tx, { organisationId: disabled.organisationId, @@ -666,7 +798,7 @@ export class DurableAgentRuntime { action: "agent.run.failed", targetType: "agent_run", targetId: disabled.id, - metadata: { failureCode: "agent_kill_switch" }, + metadata: { failureCode: eligibilityFailure.code }, traceId: redactObservationText( this.request(disabled).traceId ?? `agent-run-${disabled.id}`, ), @@ -677,8 +809,8 @@ export class DurableAgentRuntime { this.request(disabled), { status: "failed", - failureCode: "agent_kill_switch", - error: "Agent is disabled by its kill switch", + failureCode: eligibilityFailure.code, + error: eligibilityFailure.message, }, ); } @@ -819,7 +951,7 @@ export class DurableAgentRuntime { ), ); } else if (controller.signal.aborted) { - if (!this.stopping) await this.cancel(run.id); + if (!this.stopping) await this.cancel(run.id, run.organisationId); } else { await this.fail( run, @@ -940,8 +1072,10 @@ export class DurableAgentRuntime { evidence: [ { type: "fixture", - reference: "https://www.cisa.gov/known-exploited-vulnerabilities-catalog", - sha256: "0000000000000000000000000000000000000000000000000000000000000000", + reference: + "https://www.cisa.gov/known-exploited-vulnerabilities-catalog", + sha256: + "0000000000000000000000000000000000000000000000000000000000000000", }, ], }, @@ -1004,8 +1138,23 @@ export class DurableAgentRuntime { comparisonPeriod: null, }, filters: { organisationScoped: true }, - metricDefinitions: [{ key: "mtta", definition: "Synthetic", population: "Synthetic", exclusions: "Synthetic" }], - values: [{ key: "mtta", value: null, unit: "minutes", state: "unavailable", sampleSize: 0 }], + metricDefinitions: [ + { + key: "mtta", + definition: "Synthetic", + population: "Synthetic", + exclusions: "Synthetic", + }, + ], + values: [ + { + key: "mtta", + value: null, + unit: "minutes", + state: "unavailable", + sampleSize: 0, + }, + ], sourceReferences: [{ source: "synthetic", query: {} }], narrative: base.summary, caveats: ["Synthetic runtime output"], diff --git a/apps/agent-gateway/src/service-auth.test.ts b/apps/agent-gateway/src/service-auth.test.ts new file mode 100644 index 0000000..79721f2 --- /dev/null +++ b/apps/agent-gateway/src/service-auth.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { + isGatewayRequestAuthorised, + parseGatewayOrganisationId, +} from "./service-auth.ts"; + +const token = "synthetic-agent-gateway-token-at-least-32-bytes"; + +describe("agent gateway service authentication", () => { + it("accepts only the exact bearer token", () => { + expect(isGatewayRequestAuthorised(`Bearer ${token}`, token)).toBe(true); + expect(isGatewayRequestAuthorised(undefined, token)).toBe(false); + expect(isGatewayRequestAuthorised(`Basic ${token}`, token)).toBe(false); + expect(isGatewayRequestAuthorised(`Bearer ${token}x`, token)).toBe(false); + expect( + isGatewayRequestAuthorised( + "Bearer synthetic-agent-gateway-token-wrong-value", + token, + ), + ).toBe(false); + }); + + it("accepts one valid organisation UUID header", () => { + const organisationId = "019fa127-8566-770b-939a-971ce03829f6"; + expect(parseGatewayOrganisationId(organisationId)).toBe(organisationId); + expect(parseGatewayOrganisationId(undefined)).toBeNull(); + expect(parseGatewayOrganisationId("not-an-id")).toBeNull(); + expect(parseGatewayOrganisationId([organisationId])).toBeNull(); + }); +}); diff --git a/apps/agent-gateway/src/service-auth.ts b/apps/agent-gateway/src/service-auth.ts new file mode 100644 index 0000000..7d8be26 --- /dev/null +++ b/apps/agent-gateway/src/service-auth.ts @@ -0,0 +1,21 @@ +import { timingSafeEqual } from "node:crypto"; +import { z } from "zod"; + +export function isGatewayRequestAuthorised( + authorization: string | undefined, + expectedToken: string, +) { + if (!authorization?.startsWith("Bearer ")) return false; + const supplied = Buffer.from(authorization.slice("Bearer ".length), "utf8"); + const expected = Buffer.from(expectedToken, "utf8"); + return ( + supplied.length === expected.length && timingSafeEqual(supplied, expected) + ); +} + +export function parseGatewayOrganisationId( + value: string | string[] | undefined, +) { + const parsed = z.string().uuid().safeParse(value); + return parsed.success ? parsed.data : null; +} diff --git a/apps/web/app/api/v1/agent-runs/[id]/route.ts b/apps/web/app/api/v1/agent-runs/[id]/route.ts index 75e8fb6..1a845c8 100644 --- a/apps/web/app/api/v1/agent-runs/[id]/route.ts +++ b/apps/web/app/api/v1/agent-runs/[id]/route.ts @@ -8,6 +8,7 @@ import { problemResponse, requestTraceId, } from "@/lib/api-context"; +import { agentGatewayHeaders } from "@/lib/agent-gateway"; import { settleAgentRun, type AgentRunResult } from "@/lib/task-domain"; export async function GET( @@ -35,7 +36,10 @@ export async function GET( } const gateway = await fetch( `${process.env.AGENT_GATEWAY_URL ?? "http://agent-gateway:3002"}/v1/runs/${encodeURIComponent(id)}`, - { signal: AbortSignal.timeout(5_000) }, + { + headers: agentGatewayHeaders(subject.organisationId), + signal: AbortSignal.timeout(5_000), + }, ); const result = (await gateway.json()) as { status?: string; diff --git a/apps/web/app/api/v1/rooms/[id]/messages/route.ts b/apps/web/app/api/v1/rooms/[id]/messages/route.ts index 0997be4..41b32e1 100644 --- a/apps/web/app/api/v1/rooms/[id]/messages/route.ts +++ b/apps/web/app/api/v1/rooms/[id]/messages/route.ts @@ -57,46 +57,42 @@ export async function POST( ReturnType > = null; let jessieHuntError: string | null = null; - if (result.created) { + try { + agentInvocation = await new AgentDirectMessageDomainService().maybeQueue( + subject, + { messageId: result.message.id, roomId: id }, + traceId, + ); + } catch (error) { + agentInvocationError = redactObservationText( + error instanceof Error + ? error.message + : "The direct-message agent could not be queued.", + ); + } + if (result.created && !agentInvocation && !agentInvocationError) { try { - agentInvocation = - await new AgentDirectMessageDomainService().maybeQueue( - subject, - { messageId: result.message.id, roomId: id }, - traceId, - ); + jessieHunt = await new JessieHuntDomainService().maybeCreateFromMention( + subject, + { + messageId: result.message.id, + roomId: id, + plainText: input.plainText, + ...(input.relatedInvestigationId !== undefined + ? { + relatedInvestigationId: input.relatedInvestigationId, + } + : {}), + }, + traceId, + ); } catch (error) { - agentInvocationError = redactObservationText( + jessieHuntError = redactObservationText( error instanceof Error ? error.message - : "The direct-message agent could not be queued.", + : "Jessie could not prepare the hunt.", ); } - if (!agentInvocation && !agentInvocationError) { - try { - jessieHunt = - await new JessieHuntDomainService().maybeCreateFromMention( - subject, - { - messageId: result.message.id, - roomId: id, - plainText: input.plainText, - ...(input.relatedInvestigationId !== undefined - ? { - relatedInvestigationId: input.relatedInvestigationId, - } - : {}), - }, - traceId, - ); - } catch (error) { - jessieHuntError = redactObservationText( - error instanceof Error - ? error.message - : "Jessie could not prepare the hunt.", - ); - } - } } const realtimeDelivered = await publishRealtime(subject.organisationId, { type: input.threadParentId diff --git a/apps/web/app/api/v1/tasks/[id]/cancel/route.ts b/apps/web/app/api/v1/tasks/[id]/cancel/route.ts index 3eb1ef6..9d63979 100644 --- a/apps/web/app/api/v1/tasks/[id]/cancel/route.ts +++ b/apps/web/app/api/v1/tasks/[id]/cancel/route.ts @@ -7,6 +7,7 @@ import { problemResponse, requestTraceId, } from "@/lib/api-context"; +import { agentGatewayHeaders } from "@/lib/agent-gateway"; import { settleAgentRun } from "@/lib/task-domain"; export async function POST( @@ -49,6 +50,7 @@ export async function POST( const gateway = await fetch( `${process.env.AGENT_GATEWAY_URL ?? "http://agent-gateway:3002"}/v1/runs/${encodeURIComponent(task.agentRunId)}/cancel`, { + headers: agentGatewayHeaders(subject.organisationId), method: "POST", signal: AbortSignal.timeout(5_000), }, diff --git a/apps/web/app/api/v1/tasks/[id]/events/route.ts b/apps/web/app/api/v1/tasks/[id]/events/route.ts index ac4ca64..77e0288 100644 --- a/apps/web/app/api/v1/tasks/[id]/events/route.ts +++ b/apps/web/app/api/v1/tasks/[id]/events/route.ts @@ -7,6 +7,7 @@ import { problemResponse, requestTraceId, } from "@/lib/api-context"; +import { agentGatewayHeaders } from "@/lib/agent-gateway"; import { settleAgentRun, type AgentRunResult } from "@/lib/task-domain"; export const dynamic = "force-dynamic"; @@ -47,7 +48,10 @@ export async function GET( while (!request.signal.aborted) { const gateway = await fetch( `${process.env.AGENT_GATEWAY_URL ?? "http://agent-gateway:3002"}/v1/runs/${encodeURIComponent(task.agentRunId!)}`, - { signal: AbortSignal.timeout(5_000) }, + { + headers: agentGatewayHeaders(subject.organisationId), + signal: AbortSignal.timeout(5_000), + }, ); const result = (await gateway.json()) as AgentRunResult & { status: "running" | "completed" | "failed" | "cancelled"; diff --git a/apps/web/lib/agent-gateway.ts b/apps/web/lib/agent-gateway.ts new file mode 100644 index 0000000..32b3dd1 --- /dev/null +++ b/apps/web/lib/agent-gateway.ts @@ -0,0 +1,8 @@ +export function agentGatewayHeaders(organisationId: string) { + const token = process.env.MUSTER_AGENT_GATEWAY_TOKEN?.trim(); + if (!token) throw new Error("Agent gateway token is not configured"); + return { + authorization: `Bearer ${token}`, + "x-muster-organisation-id": organisationId, + }; +} diff --git a/apps/web/lib/agent-learning-domain.integration.test.ts b/apps/web/lib/agent-learning-domain.integration.test.ts index dd3d422..e7bcbaa 100644 --- a/apps/web/lib/agent-learning-domain.integration.test.ts +++ b/apps/web/lib/agent-learning-domain.integration.test.ts @@ -181,21 +181,87 @@ describeIntegration("governed agent learning", () => { .where(eq(schema.agentRuns.id, sourceRunId)); if (!source) throw new Error("Source run missing"); const queuedRunId = newId(); + const directRunId = newId(); + const directRoomId = newId(); + const directMessageId = newId(); await database() - .insert(schema.agentRuns) + .insert(schema.rooms) + .values({ + id: directRoomId, + organisationId, + name: `synthetic-learning-direct-${directRoomId}`, + slug: `synthetic-learning-direct-${directRoomId}`, + displayName: "Synthetic learning direct room", + roomType: "direct", + visibility: "private", + createdByActorId: actorId, + }); + await database() + .insert(schema.roomMemberships) + .values([ + { + organisationId, + roomId: directRoomId, + actorId, + membershipRole: "owner", + }, + { + organisationId, + roomId: directRoomId, + actorId: agentId, + membershipRole: "agent_member", + }, + ]); + await database() + .insert(schema.messages) .values({ - ...source, - id: queuedRunId, - status: "queued", - startedAt: null, - completedAt: null, - heartbeatAt: null, - leaseExpiresAt: null, - cancellationRequestedAt: null, - cancellationReason: null, - progress: { stage: "queued", percent: 0 }, - idempotencyKey: `learning-kill-switch:${queuedRunId}`, + id: directMessageId, + organisationId, + roomId: directRoomId, + authorActorId: actorId, + messageType: "text", + document: { type: "doc", content: [] }, + plainText: "Synthetic kill-switch direct request", + idempotencyKey: `learning-kill-switch-message:${directMessageId}`, }); + await database() + .insert(schema.agentRuns) + .values([ + { + ...source, + id: queuedRunId, + status: "queued", + startedAt: null, + completedAt: null, + heartbeatAt: null, + leaseExpiresAt: null, + cancellationRequestedAt: null, + cancellationReason: null, + progress: { stage: "queued", percent: 0 }, + idempotencyKey: `learning-kill-switch:${queuedRunId}`, + }, + { + ...source, + id: directRunId, + roomId: directRoomId, + trigger: "direct_message", + status: "queued", + request: { + kind: "direct_message", + sourceMessageId: directMessageId, + humanRequest: "Synthetic kill-switch direct request", + traceId: `learning-kill-switch-direct:${directRunId}`, + }, + startedAt: null, + completedAt: null, + heartbeatAt: null, + leaseExpiresAt: null, + cancellationRequestedAt: null, + cancellationReason: null, + progress: { stage: "queued", percent: 0 }, + idempotencyKey: `learning-kill-switch:${directRunId}`, + }, + ]); await mutateAgentLearning(context(), { action: "set_kill_switch", enabled: true, @@ -215,6 +281,40 @@ describeIntegration("governed agent learning", () => { status: "cancelled", reason: expect.stringContaining("kill switch"), }); + const [reply, outbox] = await Promise.all([ + database() + .select() + .from(schema.messages) + .where( + eq( + schema.messages.idempotencyKey, + `agent-direct-message-reply:${directRunId}`, + ), + ), + database() + .select() + .from(schema.outboxEvents) + .where( + eq( + schema.outboxEvents.idempotencyKey, + `room.message.created:agent-direct-message:${directRunId}`, + ), + ), + ]); + expect(reply).toHaveLength(1); + expect(reply[0]).toMatchObject({ + roomId: directRoomId, + threadParentId: directMessageId, + relatedAgentRunId: directRunId, + messageType: "agent-status", + }); + expect(reply[0]?.document).toMatchObject({ + status: "cancelled", + failureCode: "agent_kill_switch", + sourceMessageId: directMessageId, + agentRunId: directRunId, + }); + expect(outbox).toHaveLength(1); await mutateAgentLearning(context(), { action: "set_kill_switch", enabled: false, diff --git a/apps/web/lib/agent-learning-domain.ts b/apps/web/lib/agent-learning-domain.ts index e40b24a..220fc18 100644 --- a/apps/web/lib/agent-learning-domain.ts +++ b/apps/web/lib/agent-learning-domain.ts @@ -13,7 +13,7 @@ import { schema, writeOutbox, } from "@muster/database"; -import { and, desc, eq, max, or } from "drizzle-orm"; +import { and, desc, eq, gt, isNull, max, or } from "drizzle-orm"; import { z } from "zod"; const LearningMutationSchema = z.discriminatedUnion("action", [ @@ -789,7 +789,14 @@ async function setKillSwitch( ), ), ) - .returning({ id: schema.agentRuns.id }) + .returning({ + id: schema.agentRuns.id, + organisationId: schema.agentRuns.organisationId, + agentId: schema.agentRuns.agentId, + roomId: schema.agentRuns.roomId, + investigationId: schema.agentRuns.investigationId, + request: schema.agentRuns.request, + }) : []; if (cancelledRuns.length > 0) { const safeReason = redactObservationText(reason); @@ -803,6 +810,92 @@ async function setKillSwitch( payload: { reason: safeReason }, })), ); + for (const run of cancelledRuns) { + const request = z + .object({ + kind: z.literal("direct_message"), + sourceMessageId: z.string().uuid(), + traceId: z.string().optional(), + }) + .safeParse(run.request); + if (!request.success || !run.roomId) continue; + const [source] = await tx + .select({ id: schema.messages.id }) + .from(schema.messages) + .innerJoin( + schema.rooms, + and( + eq(schema.rooms.organisationId, run.organisationId), + eq(schema.rooms.id, run.roomId), + eq(schema.rooms.roomType, "direct"), + isNull(schema.rooms.archivedAt), + ), + ) + .innerJoin( + schema.roomMemberships, + and( + eq(schema.roomMemberships.organisationId, run.organisationId), + eq(schema.roomMemberships.roomId, run.roomId), + eq(schema.roomMemberships.actorId, run.agentId), + or( + isNull(schema.roomMemberships.accessExpiresAt), + gt(schema.roomMemberships.accessExpiresAt, now), + ), + ), + ) + .where( + and( + eq(schema.messages.organisationId, run.organisationId), + eq(schema.messages.id, request.data.sourceMessageId), + eq(schema.messages.roomId, run.roomId), + isNull(schema.messages.deletedAt), + ), + ) + .limit(1); + if (!source) continue; + const [message] = await tx + .insert(schema.messages) + .values({ + id: newId(), + organisationId: run.organisationId, + roomId: run.roomId, + threadParentId: request.data.sourceMessageId, + authorActorId: run.agentId, + messageType: "agent-status", + document: { + type: "agent-direct-message-reply", + status: "cancelled", + sourceMessageId: request.data.sourceMessageId, + agentRunId: run.id, + failureCode: "agent_kill_switch", + }, + plainText: "The agent request was cancelled (agent_kill_switch).", + dataClassification: "internal", + relatedInvestigationId: run.investigationId, + relatedAgentRunId: run.id, + idempotencyKey: `agent-direct-message-reply:${run.id}`, + }) + .onConflictDoNothing() + .returning({ id: schema.messages.id }); + if (!message) continue; + await writeOutbox(tx, { + organisationId: run.organisationId, + eventType: "room.message.created", + aggregateType: "message", + aggregateId: message.id, + queueName: "muster-outbox", + payload: { + messageId: message.id, + roomId: run.roomId, + threadParentId: request.data.sourceMessageId, + agentRunId: run.id, + }, + idempotencyKey: `room.message.created:agent-direct-message:${run.id}`, + traceId: redactObservationText( + request.data.traceId ?? `agent-run-${run.id}`, + ), + }); + } } await appendAuditEvent(tx, { organisationId: context.organisationId, diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 7a65ebb..7ffddf6 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -147,9 +147,12 @@ const authoritativeProcessor: Processor = async (job) => { job.queueName === "muster-agents" && job.name !== "report.generate.queued" ) { + const gatewayToken = process.env.MUSTER_AGENT_GATEWAY_TOKEN?.trim(); + if (!gatewayToken) throw new Error("Agent gateway token is not configured"); const response = await fetch( `${process.env.AGENT_GATEWAY_URL ?? "http://agent-gateway:3002"}/v1/runs/dispatch`, { + headers: { authorization: `Bearer ${gatewayToken}` }, method: "POST", signal: AbortSignal.timeout(10_000), }, diff --git a/deploy/docker/.env.homelab.example b/deploy/docker/.env.homelab.example index e273fbf..3a84c41 100644 --- a/deploy/docker/.env.homelab.example +++ b/deploy/docker/.env.homelab.example @@ -11,6 +11,7 @@ BETTER_AUTH_SECRET=generate-better-auth-secret OBJECT_STORAGE_ACCESS_KEY=muster OBJECT_STORAGE_SECRET_KEY=generate-object-storage-secret CONNECTOR_ENCRYPTION_KEY=generate-connector-encryption-key +MUSTER_AGENT_GATEWAY_TOKEN=generate-agent-gateway-token KELPIE_API_TOKEN=mock-kelpie-token TAWNY_API_TOKEN=mock-tawny-token BOWER_API_TOKEN=mock-bower-token diff --git a/deploy/docker/README.md b/deploy/docker/README.md index c80d260..8685751 100644 --- a/deploy/docker/README.md +++ b/deploy/docker/README.md @@ -11,10 +11,12 @@ MUSTER_IMAGE=ghcr.io/jusso-dev/muster@sha256:75ebdad962373ff1fa5dbef8dba8f0a005d ``` The installer creates a mode-600 `.env.homelab`, generates independent -database, authentication, storage, and administrator secrets, pulls the public -reviewed digest, starts the stack, waits for health, and creates the local -administrator. Replace the digest only after reviewing the corresponding -release evidence; mutable tags such as `latest` are rejected. +database, authentication, storage, internal agent-gateway, and administrator +secrets, pulls the public reviewed digest, starts the stack, waits for health, +and creates the local administrator. Existing installs missing +`MUSTER_AGENT_GATEWAY_TOKEN` receive one during upgrade. Replace the digest only +after reviewing the corresponding release evidence; mutable tags such as +`latest` are rejected. Only Muster's configured HTTP port is published. The example uses `http://muster.example.lan:3004`; replace it with the exact trusted browser origin. Use an HTTPS reverse proxy and set `AUTH_SECURE_COOKIES=true` when diff --git a/deploy/docker/docker-compose.homelab.yml b/deploy/docker/docker-compose.homelab.yml index cf06a12..40ce46a 100644 --- a/deploy/docker/docker-compose.homelab.yml +++ b/deploy/docker/docker-compose.homelab.yml @@ -30,6 +30,7 @@ x-muster-environment: &muster-environment MUSTER_MOCK_INTEGRATIONS: "true" MUSTER_AGENT_RUNTIME: ${MUSTER_AGENT_RUNTIME:-codex} AGENT_GATEWAY_URL: http://agent-gateway:3002 + MUSTER_AGENT_GATEWAY_TOKEN: ${MUSTER_AGENT_GATEWAY_TOKEN} CODEX_HOME: /var/lib/muster/codex x-muster-service: &muster-service diff --git a/docker-compose.yml b/docker-compose.yml index 003ba02..e78ee73 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ x-muster-environment: &muster-environment MUSTER_MOCK_INTEGRATIONS: "true" MUSTER_AGENT_RUNTIME: ${MUSTER_AGENT_RUNTIME:-codex} AGENT_GATEWAY_URL: http://agent-gateway:3002 + MUSTER_AGENT_GATEWAY_TOKEN: ${MUSTER_AGENT_GATEWAY_TOKEN:-local-agent-gateway-token-change-me-32-characters} CODEX_HOME: /var/lib/muster/codex services: diff --git a/docs/synthetic-cleanup.md b/docs/synthetic-cleanup.md index e2152d4..f899427 100644 --- a/docs/synthetic-cleanup.md +++ b/docs/synthetic-cleanup.md @@ -1,6 +1,6 @@ # Synthetic cleanup maintenance -`pnpm --filter @muster/database cleanup:synthetic` is intentionally not a selector or broad deletion tool. Supply an independently reviewed immutable JSON manifest only through the maintenance runner. +`pnpm --filter @muster/database cleanup:synthetic -- --apply /absolute/manifest.json` is intentionally not a selector or broad deletion tool. Supply an independently reviewed immutable JSON manifest only through the maintenance runner. The manifest contains one organisation, explicit UUID candidates, table digests, and a SHA-256 digest over every field except `digest`. The runner rejects digest changes and the four protected genuine direct-message IDs. diff --git a/packages/database/src/synthetic-cleanup.ts b/packages/database/src/synthetic-cleanup.ts index 932ac7f..3506d85 100644 --- a/packages/database/src/synthetic-cleanup.ts +++ b/packages/database/src/synthetic-cleanup.ts @@ -1,9 +1,10 @@ import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; import { and, eq, inArray } from "drizzle-orm"; import { z } from "zod"; import { appendAuditEvent } from "./domain-transaction.ts"; import { newId } from "./ids.ts"; -import { database } from "./index.ts"; +import { closeDatabase, database } from "./index.ts"; import { writeOutbox } from "./outbox.ts"; import * as schema from "./schema.ts"; @@ -147,3 +148,17 @@ export async function applySyntheticCleanup( return { applied: true, manifestId: manifest.manifestId }; }, { isolationLevel: "serializable", accessMode: "read write" }); } + +async function main() { + const [mode, manifestPath] = process.argv.slice(2); + if (mode !== "--apply" || !manifestPath?.startsWith("/")) { + throw new Error("Usage: cleanup:synthetic --apply /absolute/manifest.json"); + } + const input = JSON.parse(await readFile(manifestPath, "utf8")) as unknown; + const result = await applySyntheticCleanup(input, `maintenance-${newId()}`); + process.stdout.write(`${JSON.stringify(result)}\n`); +} + +if (process.argv[1]?.endsWith("synthetic-cleanup.ts")) { + await main().finally(closeDatabase); +} diff --git a/playwright.config.ts b/playwright.config.ts index 93af907..95f5de8 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -46,21 +46,21 @@ export default defineConfig({ }, { name: "Muster worker", - command: `MUSTER_RESEARCH_TEST_MODE=true CONNECTOR_ENCRYPTION_KEY=0707070707070707070707070707070707070707070707070707070707070707 DATABASE_URL=${testDatabaseUrl} REDIS_URL=${testRedisUrl} AGENT_GATEWAY_URL=http://127.0.0.1:3002 pnpm --dir apps/worker dev`, + command: `MUSTER_RESEARCH_TEST_MODE=true MUSTER_AGENT_GATEWAY_TOKEN=0707070707070707070707070707070707070707070707070707070707070707 CONNECTOR_ENCRYPTION_KEY=0707070707070707070707070707070707070707070707070707070707070707 DATABASE_URL=${testDatabaseUrl} REDIS_URL=${testRedisUrl} AGENT_GATEWAY_URL=http://127.0.0.1:3002 pnpm --dir apps/worker dev`, url: "http://127.0.0.1:3001/ready", reuseExistingServer: !process.env.CI, timeout: 120_000, }, { name: "Muster agent gateway", - command: `MUSTER_AGENT_RUNTIME=mock MUSTER_MOCK_AGENT_DELAY_MS=5000 DATABASE_URL=${testDatabaseUrl} pnpm --dir apps/agent-gateway dev`, + command: `MUSTER_AGENT_RUNTIME=mock MUSTER_AGENT_GATEWAY_TOKEN=0707070707070707070707070707070707070707070707070707070707070707 MUSTER_MOCK_AGENT_DELAY_MS=5000 DATABASE_URL=${testDatabaseUrl} pnpm --dir apps/agent-gateway dev`, url: "http://127.0.0.1:3002/ready", reuseExistingServer: !process.env.CI, timeout: 120_000, }, { name: "Muster web", - command: `MUSTER_RESEARCH_TEST_MODE=true MUSTER_DEMO_MODE=true NEXT_PUBLIC_MUSTER_DEMO_MODE=true CONNECTOR_ENCRYPTION_KEY=0707070707070707070707070707070707070707070707070707070707070707 BETTER_AUTH_SECRET=muster-playwright-secret-at-least-32-characters AUTH_RATE_LIMIT_MAX=10000 DATABASE_URL=${testDatabaseUrl} REDIS_URL=${testRedisUrl} AGENT_GATEWAY_URL=http://127.0.0.1:3002 pnpm --dir apps/web dev`, + command: `MUSTER_RESEARCH_TEST_MODE=true MUSTER_DEMO_MODE=true NEXT_PUBLIC_MUSTER_DEMO_MODE=true MUSTER_AGENT_GATEWAY_TOKEN=0707070707070707070707070707070707070707070707070707070707070707 CONNECTOR_ENCRYPTION_KEY=0707070707070707070707070707070707070707070707070707070707070707 BETTER_AUTH_SECRET=muster-playwright-secret-at-least-32-characters AUTH_RATE_LIMIT_MAX=10000 DATABASE_URL=${testDatabaseUrl} REDIS_URL=${testRedisUrl} AGENT_GATEWAY_URL=http://127.0.0.1:3002 pnpm --dir apps/web dev`, url: "http://127.0.0.1:3000/api/v1/health", reuseExistingServer: !process.env.CI, timeout: 120_000, diff --git a/scripts/install-homelab.sh b/scripts/install-homelab.sh index 2d75e42..b7f422e 100755 --- a/scripts/install-homelab.sh +++ b/scripts/install-homelab.sh @@ -37,6 +37,7 @@ if [[ ! -f "$env_file" ]]; then auth_secret="$(openssl rand -hex 32)" storage_secret="$(openssl rand -hex 24)" connector_encryption_key="$(openssl rand -hex 32)" + agent_gateway_token="$(openssl rand -hex 32)" admin_password="Muster!$(openssl rand -hex 12)" sed -i.bak \ @@ -47,6 +48,7 @@ if [[ ! -f "$env_file" ]]; then -e "s|generate-better-auth-secret|${auth_secret}|" \ -e "s|generate-object-storage-secret|${storage_secret}|" \ -e "s|generate-connector-encryption-key|${connector_encryption_key}|" \ + -e "s|generate-agent-gateway-token|${agent_gateway_token}|" \ "$env_file" rm "$env_file.bak" { @@ -62,6 +64,12 @@ if ! grep -q '^AUTH_TRUSTED_ORIGINS=' "$env_file"; then "$requested_public_url" "$requested_http_port" >> "$env_file" fi +if ! grep -q '^MUSTER_AGENT_GATEWAY_TOKEN=' "$env_file"; then + printf 'MUSTER_AGENT_GATEWAY_TOKEN=%s\n' "$(openssl rand -hex 32)" \ + >> "$env_file" + chmod 600 "$env_file" +fi + # Persist an explicitly requested reviewed image reference before sourcing the env # file. Otherwise the template's `latest` value would silently win. if [[ -n "$requested_version" ]]; then diff --git a/scripts/test-install-homelab.sh b/scripts/test-install-homelab.sh index 82f6d48..70cd943 100755 --- a/scripts/test-install-homelab.sh +++ b/scripts/test-install-homelab.sh @@ -63,6 +63,8 @@ digest="ghcr.io/jusso-dev/muster@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ./scripts/install-homelab.sh >/dev/null ) grep -Fxq "MUSTER_IMAGE=$digest" "$fixture_root/.env.homelab" +grep -Eq '^MUSTER_AGENT_GATEWAY_TOKEN=[a-f0-9]{64}$' \ + "$fixture_root/.env.homelab" if grep -q '^MUSTER_VERSION=' "$fixture_root/.env.homelab"; then printf 'Digest install retained conflicting MUSTER_VERSION.\n' >&2 exit 1 @@ -70,11 +72,16 @@ fi grep -Fxq "tawny_default" "$DOCKER_NETWORKS" grep -Fxq "kelpie_default" "$DOCKER_NETWORKS" +sed -i.bak -e '/^MUSTER_AGENT_GATEWAY_TOKEN=/d' \ + "$fixture_root/.env.homelab" +rm "$fixture_root/.env.homelab.bak" ( cd "$fixture_root" MUSTER_VERSION=sha-bbbbbbb ./scripts/install-homelab.sh >/dev/null ) grep -Fxq "MUSTER_VERSION=sha-bbbbbbb" "$fixture_root/.env.homelab" +grep -Eq '^MUSTER_AGENT_GATEWAY_TOKEN=[a-f0-9]{64}$' \ + "$fixture_root/.env.homelab" if grep -q '^MUSTER_IMAGE=' "$fixture_root/.env.homelab"; then printf 'Tag transition retained conflicting MUSTER_IMAGE.\n' >&2 exit 1 From f738614638ce0c76481a8f6f0d104a8a0aba62f4 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 14:13:37 +1000 Subject: [PATCH 06/47] feat(audit): add immutable integrity verification report --- docs/operations/incident-recovery.md | 37 ++++++ package.json | 1 + packages/audit/src/audit.test.ts | 66 ++++++++++ packages/audit/src/index.ts | 113 +++++++++++++++++- .../database/src/verify-audit-integrity.ts | 58 +++++++++ 5 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 packages/database/src/verify-audit-integrity.ts diff --git a/docs/operations/incident-recovery.md b/docs/operations/incident-recovery.md index 2ba480a..40d5f53 100644 --- a/docs/operations/incident-recovery.md +++ b/docs/operations/incident-recovery.md @@ -5,3 +5,40 @@ If Muster itself is suspected compromised: activate the agent kill switch; block Validate audit chains and compare connector delivery logs to authoritative product timelines. Rotate secrets before restoring service. Reprocess only undispatched or reconciled idempotency keys. Never replay response actions from raw queue data. Document containment, evidence custody, restoration point, tenant impact, and any audit-chain gap in the authoritative incident case. + +## Audit integrity verification and attestation + +Verify one organisation at a time after a restore or integrity incident: + +```bash +MUSTER_AUDIT_ORGANISATION_ID= pnpm db:verify-audit +``` + +The command emits an operator-visible JSON report and does not write, repair, +delete, or otherwise alter audit history. It exits `0` only for +`strict-valid`, `2` for `legacy-compatible-not-strict`, and `1` for `invalid`. + +`legacy-compatible-not-strict` identifies only the known historical defect: +an integration-action event was hashed before PostgreSQL JSONB omitted an +`approvalId: undefined` property. The report proves that specific legacy hash +is reproducible, but strict verification remains failed. It is not a repair, +does not make the historical chain strictly valid, and must never be presented +as one. + +For either non-strict result: + +1. Preserve the original report, the immutable database backup, and the + affected organisation ID with the incident evidence. +2. Record report time, application revision, verification command, strict + failure sequence, and any listed legacy sequence in an approval-backed + recovery attestation. +3. For `legacy-compatible-not-strict`, state that the result is a + compatibility reconstruction only; no audit event or outbox event was + rewritten or removed. +4. For `invalid`, treat the mismatch as unexplained: contain writes as needed, + export the affected rows/evidence, investigate source and backup lineage, + and obtain explicit incident approval before restoring service. +5. Attach the report and approval record to the authoritative incident case. + +Never update audit hashes, metadata, sequences, or outbox history merely to +make verification green. Audit history is append-only and immutable. diff --git a/package.json b/package.json index 2e6fbb9..32a999b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "db:migrate": "pnpm --filter @muster/database migrate", "db:bootstrap": "pnpm --filter @muster/database bootstrap", "db:verify-clean": "pnpm --filter @muster/database verify-clean", + "db:verify-audit": "pnpm --filter @muster/database verify:audit", "db:seed": "pnpm --filter @muster/database seed", "alfie:cisa-smoke": "node scripts/alfie-cisa-smoke.mjs", "screenshots": "playwright test tests/screenshots.spec.ts", diff --git a/packages/audit/src/audit.test.ts b/packages/audit/src/audit.test.ts index aa6e6a3..ba9f8bd 100644 --- a/packages/audit/src/audit.test.ts +++ b/packages/audit/src/audit.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { hashAuditEvent, normaliseAuditMetadata, + verifyAuditIntegrity, verifyAuditChain, type HashableAuditEvent, } from "./index"; @@ -24,6 +25,13 @@ describe("audit chain", () => { it("detects metadata tampering", () => { const event = { ...base, eventHash: hashAuditEvent(base) }; expect(verifyAuditChain([event]).valid).toBe(true); + expect(verifyAuditIntegrity([event])).toMatchObject({ + outcome: "strict-valid", + strict: { valid: true }, + legacyCompatible: { valid: true }, + legacyApprovalIdOmissions: [], + historicalChainRepaired: false, + }); expect( verifyAuditChain([{ ...event, metadata: { safe: false } }]).valid, ).toBe(false); @@ -51,4 +59,62 @@ describe("audit chain", () => { ]), ).toEqual({ valid: true }); }); + + it("reports the legacy undefined approvalId defect without calling it repaired", () => { + const historicalInput = { + ...base, + action: "integration.action.queued", + metadata: { + integrationId: "integration", + operation: "alerts.list", + capability: "alerts.read", + approvalId: undefined, + }, + }; + const historicalEvent = { + ...historicalInput, + metadata: { + integrationId: "integration", + operation: "alerts.list", + capability: "alerts.read", + }, + eventHash: hashAuditEvent(historicalInput), + }; + const followingInput = { + ...base, + sequence: 2, + targetId: "delivery", + previousHash: historicalEvent.eventHash, + }; + const followingEvent = { + ...followingInput, + eventHash: hashAuditEvent(followingInput), + }; + + expect(verifyAuditChain([historicalEvent, followingEvent])).toEqual({ + valid: false, + brokenAt: 1, + }); + expect( + verifyAuditIntegrity([historicalEvent, followingEvent]), + ).toMatchObject({ + outcome: "legacy-compatible-not-strict", + strict: { valid: false, brokenAt: 1 }, + legacyCompatible: { valid: true }, + legacyApprovalIdOmissions: [{ sequence: 1 }], + historicalChainRepaired: false, + }); + }); + + it("does not accept an unexplained hash mismatch as legacy-compatible", () => { + const event = { ...base, eventHash: "0".repeat(64) }; + + expect(verifyAuditIntegrity([event])).toMatchObject({ + outcome: "invalid", + strict: { valid: false, brokenAt: 1 }, + legacyCompatible: { valid: false, brokenAt: 1 }, + legacyApprovalIdOmissions: [], + historicalChainRepaired: false, + }); + }); }); diff --git a/packages/audit/src/index.ts b/packages/audit/src/index.ts index 342c341..a787a90 100644 --- a/packages/audit/src/index.ts +++ b/packages/audit/src/index.ts @@ -14,6 +14,25 @@ export interface HashableAuditEvent { createdAt: string; } +export type AuditChainVerification = + | { valid: true } + | { valid: false; brokenAt: number }; + +export interface AuditLegacyCompatibilityMatch { + sequence: number; +} + +export interface AuditIntegrityReport { + outcome: "strict-valid" | "legacy-compatible-not-strict" | "invalid"; + strict: AuditChainVerification; + legacyCompatible: AuditChainVerification; + legacyApprovalIdOmissions: ReadonlyArray; + historicalChainRepaired: false; + attestation: string; +} + +type PersistedAuditEvent = HashableAuditEvent & { eventHash: string }; + function canonical(value: unknown): string { if (value === null || typeof value !== "object") return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; @@ -33,19 +52,101 @@ export function normaliseAuditMetadata(metadata: unknown): unknown { return JSON.parse(serialised) as unknown; } -export function verifyAuditChain( - events: ReadonlyArray, -): { valid: boolean; brokenAt?: number } { +function isLegacyApprovalIdOmission(event: PersistedAuditEvent): boolean { + if ( + event.metadata === null || + typeof event.metadata !== "object" || + Array.isArray(event.metadata) || + Object.hasOwn(event.metadata, "approvalId") + ) { + return false; + } + const { eventHash, ...hashable } = event; + return ( + hashAuditEvent({ + ...hashable, + metadata: { ...event.metadata, approvalId: undefined }, + }) === eventHash + ); +} + +function verify( + events: ReadonlyArray, + allowLegacyApprovalIdOmission: boolean, +): { + verification: AuditChainVerification; + legacyApprovalIdOmissions: AuditLegacyCompatibilityMatch[]; +} { + const legacyApprovalIdOmissions: AuditLegacyCompatibilityMatch[] = []; let previousHash = "0".repeat(64); for (const event of events) { if (event.previousHash !== previousHash) { - return { valid: false, brokenAt: event.sequence }; + return { + verification: { valid: false, brokenAt: event.sequence }, + legacyApprovalIdOmissions, + }; } const { eventHash, ...hashable } = event; if (hashAuditEvent(hashable) !== eventHash) { - return { valid: false, brokenAt: event.sequence }; + if ( + !allowLegacyApprovalIdOmission || + !isLegacyApprovalIdOmission(event) + ) { + return { + verification: { valid: false, brokenAt: event.sequence }, + legacyApprovalIdOmissions, + }; + } + legacyApprovalIdOmissions.push({ sequence: event.sequence }); } previousHash = eventHash; } - return { valid: true }; + return { verification: { valid: true }, legacyApprovalIdOmissions }; +} + +export function verifyAuditChain( + events: ReadonlyArray, +): AuditChainVerification { + return verify(events, false).verification; +} + +export function verifyAuditIntegrity( + events: ReadonlyArray, +): AuditIntegrityReport { + const strict = verify(events, false).verification; + const legacy = verify(events, true); + + if (strict.valid) { + return { + outcome: "strict-valid", + strict, + legacyCompatible: legacy.verification, + legacyApprovalIdOmissions: [], + historicalChainRepaired: false, + attestation: + "Strict audit-chain verification passed. No historical event was repaired or changed.", + }; + } + + if (legacy.verification.valid && legacy.legacyApprovalIdOmissions.length > 0) { + return { + outcome: "legacy-compatible-not-strict", + strict, + legacyCompatible: legacy.verification, + legacyApprovalIdOmissions: legacy.legacyApprovalIdOmissions, + historicalChainRepaired: false, + attestation: + "A known pre-normalisation undefined approvalId hash is reproducible. Strict verification still fails; this does not repair or change immutable audit history.", + }; + } + + return { + outcome: "invalid", + strict, + legacyCompatible: legacy.verification, + legacyApprovalIdOmissions: legacy.legacyApprovalIdOmissions, + historicalChainRepaired: false, + attestation: + "Strict verification failed and no known legacy compatibility reconstruction explains the chain. Preserve immutable history and investigate before attesting recovery.", + }; } diff --git a/packages/database/src/verify-audit-integrity.ts b/packages/database/src/verify-audit-integrity.ts new file mode 100644 index 0000000..932dc93 --- /dev/null +++ b/packages/database/src/verify-audit-integrity.ts @@ -0,0 +1,58 @@ +import { asc, eq } from "drizzle-orm"; +import { verifyAuditIntegrity } from "@muster/audit"; +import { closeDatabase, database, schema } from "./index.ts"; + +const organisationId = process.env.MUSTER_AUDIT_ORGANISATION_ID; + +if (!organisationId) { + throw new Error("MUSTER_AUDIT_ORGANISATION_ID is required"); +} + +try { + const events = await database() + .select({ + organisationId: schema.auditEvents.organisationId, + sequence: schema.auditEvents.sequence, + actorId: schema.auditEvents.actorId, + actorType: schema.auditEvents.actorType, + action: schema.auditEvents.action, + targetType: schema.auditEvents.targetType, + targetId: schema.auditEvents.targetId, + previousHash: schema.auditEvents.previousHash, + eventHash: schema.auditEvents.eventHash, + metadata: schema.auditEvents.metadata, + traceId: schema.auditEvents.traceId, + createdAt: schema.auditEvents.createdAt, + }) + .from(schema.auditEvents) + .where(eq(schema.auditEvents.organisationId, organisationId)) + .orderBy(asc(schema.auditEvents.sequence)); + + const report = verifyAuditIntegrity( + events.map((event) => ({ + ...event, + createdAt: event.createdAt.toISOString(), + })), + ); + + process.stdout.write( + `${JSON.stringify( + { + organisationId, + eventCount: events.length, + verifiedAt: new Date().toISOString(), + ...report, + }, + null, + 2, + )}\n`, + ); + process.exitCode = + report.outcome === "strict-valid" + ? 0 + : report.outcome === "legacy-compatible-not-strict" + ? 2 + : 1; +} finally { + await closeDatabase(); +} From aec5adbcb5a6df0fe9fdd123c9669a5067f5ec7b Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 14:15:46 +1000 Subject: [PATCH 07/47] fix(audit): reject legacy scope and sequence drift --- packages/audit/src/audit.test.ts | 36 ++++++++++++++++++++++++++++++++ packages/audit/src/index.ts | 20 ++++++++++++++---- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/audit/src/audit.test.ts b/packages/audit/src/audit.test.ts index ba9f8bd..8fd3a49 100644 --- a/packages/audit/src/audit.test.ts +++ b/packages/audit/src/audit.test.ts @@ -117,4 +117,40 @@ describe("audit chain", () => { historicalChainRepaired: false, }); }); + + it("rejects sequence gaps even when event hashes link", () => { + const first = { ...base, eventHash: hashAuditEvent(base) }; + const thirdInput = { + ...base, + sequence: 3, + previousHash: first.eventHash, + }; + const third = { ...thirdInput, eventHash: hashAuditEvent(thirdInput) }; + + expect(verifyAuditIntegrity([first, third])).toMatchObject({ + outcome: "invalid", + strict: { valid: false, brokenAt: 3 }, + legacyCompatible: { valid: false, brokenAt: 3 }, + }); + }); + + it("limits legacy compatibility to known integration-action events", () => { + const historicalInput = { + ...base, + action: "unexpected.action", + metadata: { operation: "alerts.list", approvalId: undefined }, + }; + const persisted = { + ...historicalInput, + metadata: { operation: "alerts.list" }, + eventHash: hashAuditEvent(historicalInput), + }; + + expect(verifyAuditIntegrity([persisted])).toMatchObject({ + outcome: "invalid", + strict: { valid: false, brokenAt: 1 }, + legacyCompatible: { valid: false, brokenAt: 1 }, + legacyApprovalIdOmissions: [], + }); + }); }); diff --git a/packages/audit/src/index.ts b/packages/audit/src/index.ts index a787a90..cf22795 100644 --- a/packages/audit/src/index.ts +++ b/packages/audit/src/index.ts @@ -15,8 +15,7 @@ export interface HashableAuditEvent { } export type AuditChainVerification = - | { valid: true } - | { valid: false; brokenAt: number }; + { valid: true } | { valid: false; brokenAt: number }; export interface AuditLegacyCompatibilityMatch { sequence: number; @@ -32,6 +31,10 @@ export interface AuditIntegrityReport { } type PersistedAuditEvent = HashableAuditEvent & { eventHash: string }; +const legacyApprovalIdActions = new Set([ + "integration.action.queued", + "integration.action.succeeded", +]); function canonical(value: unknown): string { if (value === null || typeof value !== "object") return JSON.stringify(value); @@ -54,6 +57,7 @@ export function normaliseAuditMetadata(metadata: unknown): unknown { function isLegacyApprovalIdOmission(event: PersistedAuditEvent): boolean { if ( + !legacyApprovalIdActions.has(event.action) || event.metadata === null || typeof event.metadata !== "object" || Array.isArray(event.metadata) || @@ -79,8 +83,12 @@ function verify( } { const legacyApprovalIdOmissions: AuditLegacyCompatibilityMatch[] = []; let previousHash = "0".repeat(64); + let expectedSequence = 1; for (const event of events) { - if (event.previousHash !== previousHash) { + if ( + event.sequence !== expectedSequence || + event.previousHash !== previousHash + ) { return { verification: { valid: false, brokenAt: event.sequence }, legacyApprovalIdOmissions, @@ -100,6 +108,7 @@ function verify( legacyApprovalIdOmissions.push({ sequence: event.sequence }); } previousHash = eventHash; + expectedSequence += 1; } return { verification: { valid: true }, legacyApprovalIdOmissions }; } @@ -128,7 +137,10 @@ export function verifyAuditIntegrity( }; } - if (legacy.verification.valid && legacy.legacyApprovalIdOmissions.length > 0) { + if ( + legacy.verification.valid && + legacy.legacyApprovalIdOmissions.length > 0 + ) { return { outcome: "legacy-compatible-not-strict", strict, From cfac706263cc5f65b93cdc0a469d3592e621d51d Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 14:23:16 +1000 Subject: [PATCH 08/47] feat: archive synthetic artifact lists --- .../app/api/v1/agents/[id]/learning/route.ts | 7 +- apps/web/app/api/v1/hunts/route.ts | 9 +- apps/web/app/api/v1/reports/route.ts | 40 +- .../web/app/api/v1/reports/schedules/route.ts | 35 +- apps/web/app/api/v1/tasks/route.ts | 9 +- apps/web/lib/agent-learning-domain.ts | 23 +- apps/web/lib/alfie-research-domain.ts | 7 +- apps/web/lib/archive-visibility.test.ts | 43 + apps/web/lib/connector-domain.ts | 7 +- .../migrations/0017_daily_dexter_bennett.sql | 6 + .../migrations/meta/0017_snapshot.json | 11199 ++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + packages/database/src/schema.ts | 6 + 13 files changed, 11366 insertions(+), 32 deletions(-) create mode 100644 apps/web/lib/archive-visibility.test.ts create mode 100644 packages/database/migrations/0017_daily_dexter_bennett.sql create mode 100644 packages/database/migrations/meta/0017_snapshot.json diff --git a/apps/web/app/api/v1/agents/[id]/learning/route.ts b/apps/web/app/api/v1/agents/[id]/learning/route.ts index 4e428b6..9c44869 100644 --- a/apps/web/app/api/v1/agents/[id]/learning/route.ts +++ b/apps/web/app/api/v1/agents/[id]/learning/route.ts @@ -22,7 +22,12 @@ export async function GET( const subject = await apiSubject(request); requireCapability(subject, "agents.read"); const { id } = await params; - const data = await agentLearningState(subject.organisationId, id); + const includeInactive = + new URL(request.url).searchParams.get("includeInactive") === "true"; + if (includeInactive) requireCapability(subject, "agents.manage"); + const data = await agentLearningState(subject.organisationId, id, { + includeInactive, + }); return Response.json({ data, traceId }); } catch (error) { return problemResponse(error, traceId); diff --git a/apps/web/app/api/v1/hunts/route.ts b/apps/web/app/api/v1/hunts/route.ts index 01ba1ed..e454ad9 100644 --- a/apps/web/app/api/v1/hunts/route.ts +++ b/apps/web/app/api/v1/hunts/route.ts @@ -1,6 +1,6 @@ import { requireCapability } from "@muster/authz"; import { database, schema } from "@muster/database"; -import { desc, eq } from "drizzle-orm"; +import { and, desc, eq, isNull } from "drizzle-orm"; import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; import { JessieHuntDomainService } from "@/lib/jessie-hunt-domain"; @@ -12,7 +12,12 @@ export async function GET(request: Request) { const rows = await database() .select() .from(schema.huntRuns) - .where(eq(schema.huntRuns.organisationId, subject.organisationId)) + .where( + and( + eq(schema.huntRuns.organisationId, subject.organisationId), + isNull(schema.huntRuns.archivedAt), + ), + ) .orderBy(desc(schema.huntRuns.createdAt)) .limit(100); return Response.json({ data: rows, traceId }); diff --git a/apps/web/app/api/v1/reports/route.ts b/apps/web/app/api/v1/reports/route.ts index fa0b83a..71a3ccd 100644 --- a/apps/web/app/api/v1/reports/route.ts +++ b/apps/web/app/api/v1/reports/route.ts @@ -1,6 +1,6 @@ import { requireCapability } from "@muster/authz"; import { database, schema } from "@muster/database"; -import { and, desc, eq } from "drizzle-orm"; +import { and, desc, eq, isNull } from "drizzle-orm"; import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; import { ParkerReportDomainService } from "@/lib/parker-report-domain"; @@ -9,25 +9,47 @@ export async function GET(request: Request) { try { const subject = await apiSubject(request); requireCapability(subject, "agents.read"); - const data = await database().select({ report: schema.reportManifests }).from(schema.reportManifests) + const data = await database() + .select({ report: schema.reportManifests }) + .from(schema.reportManifests) .innerJoin( schema.roomMemberships, and( - eq(schema.roomMemberships.organisationId, schema.reportManifests.organisationId), + eq( + schema.roomMemberships.organisationId, + schema.reportManifests.organisationId, + ), eq(schema.roomMemberships.roomId, schema.reportManifests.roomId), eq(schema.roomMemberships.actorId, subject.actorId), ), ) - .where(eq(schema.reportManifests.organisationId, subject.organisationId)) - .orderBy(desc(schema.reportManifests.createdAt)).limit(100); + .where( + and( + eq(schema.reportManifests.organisationId, subject.organisationId), + isNull(schema.reportManifests.archivedAt), + ), + ) + .orderBy(desc(schema.reportManifests.createdAt)) + .limit(100); return Response.json({ data: data.map((row) => row.report), traceId }); - } catch (error) { return problemResponse(error, traceId); } + } catch (error) { + return problemResponse(error, traceId); + } } export async function POST(request: Request) { const traceId = requestTraceId(request); try { - const result = await new ParkerReportDomainService().create(await apiSubject(request), await request.json(), traceId); - return Response.json({ data: result, traceId }, { status: result.duplicate ? 200 : 202 }); - } catch (error) { return problemResponse(error, traceId); } + const result = await new ParkerReportDomainService().create( + await apiSubject(request), + await request.json(), + traceId, + ); + return Response.json( + { data: result, traceId }, + { status: result.duplicate ? 200 : 202 }, + ); + } catch (error) { + return problemResponse(error, traceId); + } } diff --git a/apps/web/app/api/v1/reports/schedules/route.ts b/apps/web/app/api/v1/reports/schedules/route.ts index 05b50e8..93518eb 100644 --- a/apps/web/app/api/v1/reports/schedules/route.ts +++ b/apps/web/app/api/v1/reports/schedules/route.ts @@ -1,21 +1,42 @@ import { requireCapability } from "@muster/authz"; import { database, schema } from "@muster/database"; -import { and, desc, eq } from "drizzle-orm"; +import { and, desc, eq, isNull } from "drizzle-orm"; import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; import { ParkerReportDomainService } from "@/lib/parker-report-domain"; export async function GET(request: Request) { const traceId = requestTraceId(request); try { - const subject = await apiSubject(request); requireCapability(subject, "administration.manage"); - const data = await database().select().from(schema.reportSchedules).where(eq(schema.reportSchedules.organisationId, subject.organisationId)).orderBy(desc(schema.reportSchedules.nextRunAt)); + const subject = await apiSubject(request); + requireCapability(subject, "administration.manage"); + const data = await database() + .select() + .from(schema.reportSchedules) + .where( + and( + eq(schema.reportSchedules.organisationId, subject.organisationId), + isNull(schema.reportSchedules.archivedAt), + ), + ) + .orderBy(desc(schema.reportSchedules.nextRunAt)); return Response.json({ data, traceId }); - } catch (error) { return problemResponse(error, traceId); } + } catch (error) { + return problemResponse(error, traceId); + } } export async function POST(request: Request) { const traceId = requestTraceId(request); try { - const result = await new ParkerReportDomainService().createSchedule(await apiSubject(request), await request.json(), traceId); - return Response.json({ data: result, traceId }, { status: result.duplicate ? 200 : 201 }); - } catch (error) { return problemResponse(error, traceId); } + const result = await new ParkerReportDomainService().createSchedule( + await apiSubject(request), + await request.json(), + traceId, + ); + return Response.json( + { data: result, traceId }, + { status: result.duplicate ? 200 : 201 }, + ); + } catch (error) { + return problemResponse(error, traceId); + } } diff --git a/apps/web/app/api/v1/tasks/route.ts b/apps/web/app/api/v1/tasks/route.ts index c286b65..09b2123 100644 --- a/apps/web/app/api/v1/tasks/route.ts +++ b/apps/web/app/api/v1/tasks/route.ts @@ -1,4 +1,4 @@ -import { and, asc, eq, inArray } from "drizzle-orm"; +import { and, asc, eq, inArray, isNull } from "drizzle-orm"; import { requireCapability } from "@muster/authz"; import { redactForObservation } from "@muster/config"; import { TaskPrioritySchema, TaskStatusSchema } from "@muster/contracts"; @@ -28,7 +28,12 @@ async function taskView(organisationId: string, includeEvidence: boolean) { const rows = await db .select() .from(schema.tasks) - .where(eq(schema.tasks.organisationId, organisationId)) + .where( + and( + eq(schema.tasks.organisationId, organisationId), + isNull(schema.tasks.archivedAt), + ), + ) .orderBy(asc(schema.tasks.status), asc(schema.tasks.createdAt)); const actorIds = rows .map((task) => task.assignedActorId) diff --git a/apps/web/lib/agent-learning-domain.ts b/apps/web/lib/agent-learning-domain.ts index 220fc18..fa87a90 100644 --- a/apps/web/lib/agent-learning-domain.ts +++ b/apps/web/lib/agent-learning-domain.ts @@ -13,7 +13,7 @@ import { schema, writeOutbox, } from "@muster/database"; -import { and, desc, eq, gt, isNull, max, or } from "drizzle-orm"; +import { and, desc, eq, gt, isNull, max, ne, or } from "drizzle-orm"; import { z } from "zod"; const LearningMutationSchema = z.discriminatedUnion("action", [ @@ -63,8 +63,22 @@ function strings(value: unknown): string[] { export async function agentLearningState( organisationId: string, agentId: string, + options: { includeInactive?: boolean } = {}, ) { const db = database(); + const memoryConditions = [ + eq(schema.agentMemories.organisationId, organisationId), + eq(schema.agentMemories.agentId, agentId), + ]; + if (!options.includeInactive) { + const now = new Date(); + memoryConditions.push(ne(schema.agentMemories.status, "rejected")); + const activeMemoryCondition = or( + isNull(schema.agentMemories.expiresAt), + gt(schema.agentMemories.expiresAt, now), + ); + if (activeMemoryCondition) memoryConditions.push(activeMemoryCondition); + } const [definition] = await db .select() .from(schema.agentDefinitions) @@ -81,12 +95,7 @@ export async function agentLearningState( db .select() .from(schema.agentMemories) - .where( - and( - eq(schema.agentMemories.organisationId, organisationId), - eq(schema.agentMemories.agentId, agentId), - ), - ) + .where(and(...memoryConditions)) .orderBy(desc(schema.agentMemories.createdAt)) .limit(100), db diff --git a/apps/web/lib/alfie-research-domain.ts b/apps/web/lib/alfie-research-domain.ts index 12f396e..2cab3a8 100644 --- a/apps/web/lib/alfie-research-domain.ts +++ b/apps/web/lib/alfie-research-domain.ts @@ -7,7 +7,7 @@ import { schema, writeOutbox, } from "@muster/database"; -import { and, eq } from "drizzle-orm"; +import { and, eq, isNull } from "drizzle-orm"; import { z } from "zod"; import { ApiProblem } from "./api-context"; @@ -90,7 +90,10 @@ export class AlfieResearchDomainService { .select() .from(schema.researchWatchlists) .where( - eq(schema.researchWatchlists.organisationId, subject.organisationId), + and( + eq(schema.researchWatchlists.organisationId, subject.organisationId), + isNull(schema.researchWatchlists.archivedAt), + ), ); } diff --git a/apps/web/lib/archive-visibility.test.ts b/apps/web/lib/archive-visibility.test.ts new file mode 100644 index 0000000..56c8d65 --- /dev/null +++ b/apps/web/lib/archive-visibility.test.ts @@ -0,0 +1,43 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const repositoryRoot = fileURLToPath(new URL("../../../", import.meta.url)); + +function repositoryFile(path: string) { + return readFileSync(new URL(path, `file://${repositoryRoot}/`), "utf8"); +} + +describe("archived synthetic artifact visibility", () => { + it("excludes archived parent records from normal collection queries", () => { + const collectionQueries = { + "apps/web/app/api/v1/tasks/route.ts": "schema.tasks.archivedAt", + "apps/web/app/api/v1/hunts/route.ts": "schema.huntRuns.archivedAt", + "apps/web/lib/connector-domain.ts": + "schema.integrationRecords.archivedAt", + "apps/web/lib/alfie-research-domain.ts": + "schema.researchWatchlists.archivedAt", + "apps/web/app/api/v1/reports/route.ts": + "schema.reportManifests.archivedAt", + "apps/web/app/api/v1/reports/schedules/route.ts": + "schema.reportSchedules.archivedAt", + }; + + for (const [path, column] of Object.entries(collectionQueries)) { + expect(repositoryFile(path)).toContain(`isNull(${column})`); + } + }); + + it("keeps inactive learning history behind an explicit managed option", () => { + const domain = repositoryFile("apps/web/lib/agent-learning-domain.ts"); + const route = repositoryFile( + "apps/web/app/api/v1/agents/[id]/learning/route.ts", + ); + + expect(domain).toContain("includeInactive?: boolean"); + expect(domain).toContain('ne(schema.agentMemories.status, "rejected")'); + expect(domain).toContain("gt(schema.agentMemories.expiresAt, now)"); + expect(route).toContain("includeInactive"); + expect(route).toContain('requireCapability(subject, "agents.manage")'); + }); +}); diff --git a/apps/web/lib/connector-domain.ts b/apps/web/lib/connector-domain.ts index 6e3d424..7ac6ecb 100644 --- a/apps/web/lib/connector-domain.ts +++ b/apps/web/lib/connector-domain.ts @@ -1,4 +1,4 @@ -import { and, count, desc, eq, gte, sql } from "drizzle-orm"; +import { and, count, desc, eq, gte, isNull, sql } from "drizzle-orm"; import { requireCapability, type AuthorisationSubject } from "@muster/authz"; import { appendAuditEvent, @@ -43,7 +43,10 @@ export class ConnectorDomainService { .select() .from(schema.integrationRecords) .where( - eq(schema.integrationRecords.organisationId, subject.organisationId), + and( + eq(schema.integrationRecords.organisationId, subject.organisationId), + isNull(schema.integrationRecords.archivedAt), + ), ) .orderBy(desc(schema.integrationRecords.updatedAt)); return records.map((record) => ({ diff --git a/packages/database/migrations/0017_daily_dexter_bennett.sql b/packages/database/migrations/0017_daily_dexter_bennett.sql new file mode 100644 index 0000000..5ba743a --- /dev/null +++ b/packages/database/migrations/0017_daily_dexter_bennett.sql @@ -0,0 +1,6 @@ +ALTER TABLE "hunt_runs" ADD COLUMN "archived_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "integration_records" ADD COLUMN "archived_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "report_manifests" ADD COLUMN "archived_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "report_schedules" ADD COLUMN "archived_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "research_watchlists" ADD COLUMN "archived_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "tasks" ADD COLUMN "archived_at" timestamp with time zone; \ No newline at end of file diff --git a/packages/database/migrations/meta/0017_snapshot.json b/packages/database/migrations/meta/0017_snapshot.json new file mode 100644 index 0000000..ace2a3a --- /dev/null +++ b/packages/database/migrations/meta/0017_snapshot.json @@ -0,0 +1,11199 @@ +{ + "id": "3f61ffb8-af0d-45ef-a0be-7f325899949f", + "prevId": "4d1b8c61-00dd-4090-a541-85aed810ed71", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.actors": { + "name": "actors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "identity_reference": { + "name": "identity_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capability_assignments": { + "name": "capability_assignments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "actors_org_type_idx": { + "name": "actors_org_type_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "actors_org_identity_unique": { + "name": "actors_org_identity_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "identity_reference", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"actors\".\"identity_reference\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "actors_organisation_id_organisations_id_fk": { + "name": "actors_organisation_id_organisations_id_fk", + "tableFrom": "actors", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_definitions": { + "name": "agent_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_actor_id": { + "name": "owner_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "system_prompt_version": { + "name": "system_prompt_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "allowed_rooms": { + "name": "allowed_rooms", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "capability_requirements": { + "name": "capability_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "maximum_runtime_seconds": { + "name": "maximum_runtime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "maximum_token_budget": { + "name": "maximum_token_budget", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20000 + }, + "maximum_cost_cents": { + "name": "maximum_cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 500 + }, + "data_classification_allowance": { + "name": "data_classification_allowance", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"internal\"]'::jsonb" + }, + "approval_requirements": { + "name": "approval_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "requested_permission_mode": { + "name": "requested_permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read_only'" + }, + "kill_switch": { + "name": "kill_switch", + "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": { + "agent_definitions_org_name_unique": { + "name": "agent_definitions_org_name_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_definitions_organisation_id_organisations_id_fk": { + "name": "agent_definitions_organisation_id_organisations_id_fk", + "tableFrom": "agent_definitions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_definitions_owner_actor_id_actors_id_fk": { + "name": "agent_definitions_owner_actor_id_actors_id_fk", + "tableFrom": "agent_definitions", + "tableTo": "actors", + "columnsFrom": [ + "owner_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_definitions_requested_permission_check": { + "name": "agent_definitions_requested_permission_check", + "value": "\"agent_definitions\".\"requested_permission_mode\" in ('read_only','approval_gated')" + } + }, + "isRLSEnabled": false + }, + "public.agent_memories": { + "name": "agent_memories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence_references": { + "name": "evidence_references", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "supersedes_memory_id": { + "name": "supersedes_memory_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reviewed_by_actor_id": { + "name": "reviewed_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_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": { + "agent_memories_org_agent_idx": { + "name": "agent_memories_org_agent_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_memories_organisation_id_organisations_id_fk": { + "name": "agent_memories_organisation_id_organisations_id_fk", + "tableFrom": "agent_memories", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_memories_agent_id_agent_definitions_id_fk": { + "name": "agent_memories_agent_id_agent_definitions_id_fk", + "tableFrom": "agent_memories", + "tableTo": "agent_definitions", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_memories_source_run_id_agent_runs_id_fk": { + "name": "agent_memories_source_run_id_agent_runs_id_fk", + "tableFrom": "agent_memories", + "tableTo": "agent_runs", + "columnsFrom": [ + "source_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_memories_reviewed_by_actor_id_actors_id_fk": { + "name": "agent_memories_reviewed_by_actor_id_actors_id_fk", + "tableFrom": "agent_memories", + "tableTo": "actors", + "columnsFrom": [ + "reviewed_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_memories_kind_check": { + "name": "agent_memories_kind_check", + "value": "\"agent_memories\".\"kind\" in ('fact','preference','lesson','failure','procedure_hint')" + }, + "agent_memories_status_check": { + "name": "agent_memories_status_check", + "value": "\"agent_memories\".\"status\" in ('active','superseded','expired','rejected')" + }, + "agent_memories_confidence_check": { + "name": "agent_memories_confidence_check", + "value": "\"agent_memories\".\"confidence\" between 0 and 100" + } + }, + "isRLSEnabled": false + }, + "public.agent_readiness_snapshots": { + "name": "agent_readiness_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "process_identity": { + "name": "process_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gateway_state": { + "name": "gateway_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authentication_state": { + "name": "authentication_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observer_state": { + "name": "observer_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle_evidence_state": { + "name": "lifecycle_evidence_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle_state": { + "name": "lifecycle_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capability_state": { + "name": "capability_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_state": { + "name": "tool_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_state": { + "name": "permission_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_runtime": { + "name": "reported_runtime", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_provider": { + "name": "reported_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_model": { + "name": "reported_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_capabilities": { + "name": "input_capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_capabilities": { + "name": "output_capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "available_commands": { + "name": "available_commands", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "tool_sources": { + "name": "tool_sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "tool_risk_classes": { + "name": "tool_risk_classes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "requested_permission_mode": { + "name": "requested_permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_permission_mode": { + "name": "effective_permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limitations": { + "name": "limitations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_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()" + } + }, + "indexes": { + "agent_readiness_org_agent_verified_idx": { + "name": "agent_readiness_org_agent_verified_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verified_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_readiness_org_process_verified_idx": { + "name": "agent_readiness_org_process_verified_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "process_identity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verified_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_readiness_snapshots_organisation_id_organisations_id_fk": { + "name": "agent_readiness_snapshots_organisation_id_organisations_id_fk", + "tableFrom": "agent_readiness_snapshots", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_readiness_snapshots_agent_id_agent_definitions_id_fk": { + "name": "agent_readiness_snapshots_agent_id_agent_definitions_id_fk", + "tableFrom": "agent_readiness_snapshots", + "tableTo": "agent_definitions", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_readiness_evidence_states_check": { + "name": "agent_readiness_evidence_states_check", + "value": "\"agent_readiness_snapshots\".\"gateway_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"authentication_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"observer_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"lifecycle_evidence_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"capability_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"tool_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"permission_state\" in ('reported','unavailable','unknown')" + }, + "agent_readiness_lifecycle_state_check": { + "name": "agent_readiness_lifecycle_state_check", + "value": "\"agent_readiness_snapshots\".\"lifecycle_state\" in ('idle','running','stopped','failed','unknown')" + }, + "agent_readiness_permission_modes_check": { + "name": "agent_readiness_permission_modes_check", + "value": "\"agent_readiness_snapshots\".\"requested_permission_mode\" in ('read_only','approval_gated','unknown')\n and \"agent_readiness_snapshots\".\"effective_permission_mode\" in ('read_only','approval_gated','unknown')" + } + }, + "isRLSEnabled": false + }, + "public.agent_run_events": { + "name": "agent_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_run_events_org_run_idx": { + "name": "agent_run_events_org_run_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_events_organisation_id_organisations_id_fk": { + "name": "agent_run_events_organisation_id_organisations_id_fk", + "tableFrom": "agent_run_events", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_run_events_run_id_agent_runs_id_fk": { + "name": "agent_run_events_run_id_agent_runs_id_fk", + "tableFrom": "agent_run_events", + "tableTo": "agent_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_run_sources": { + "name": "agent_run_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_run_sources_org_run_unique": { + "name": "agent_run_sources_org_run_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_run_sources_org_run_idx": { + "name": "agent_run_sources_org_run_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_sources_organisation_id_organisations_id_fk": { + "name": "agent_run_sources_organisation_id_organisations_id_fk", + "tableFrom": "agent_run_sources", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_run_sources_run_id_agent_runs_id_fk": { + "name": "agent_run_sources_run_id_agent_runs_id_fk", + "tableFrom": "agent_run_sources", + "tableTo": "agent_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runs": { + "name": "agent_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "progress": { + "name": "progress", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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 + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline_at": { + "name": "deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "worker_id": { + "name": "worker_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt_hash": { + "name": "prompt_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_hash": { + "name": "output_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_schema": { + "name": "output_schema", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "maximum_runtime_seconds": { + "name": "maximum_runtime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "maximum_token_budget": { + "name": "maximum_token_budget", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20000 + }, + "maximum_cost_cents": { + "name": "maximum_cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 500 + }, + "token_usage": { + "name": "token_usage", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "estimated_cost_cents": { + "name": "estimated_cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_call_count": { + "name": "tool_call_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "diagnostics": { + "name": "diagnostics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "structured_output": { + "name": "structured_output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_runs_org_idempotency_unique": { + "name": "agent_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runs_org_status_idx": { + "name": "agent_runs_org_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runs_recovery_idx": { + "name": "agent_runs_recovery_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runs_agent_id_agent_definitions_id_fk": { + "name": "agent_runs_agent_id_agent_definitions_id_fk", + "tableFrom": "agent_runs", + "tableTo": "agent_definitions", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runs_organisation_id_organisations_id_fk": { + "name": "agent_runs_organisation_id_organisations_id_fk", + "tableFrom": "agent_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runs_room_id_rooms_id_fk": { + "name": "agent_runs_room_id_rooms_id_fk", + "tableFrom": "agent_runs", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runs_investigation_id_investigations_id_fk": { + "name": "agent_runs_investigation_id_investigations_id_fk", + "tableFrom": "agent_runs", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runs_requested_by_actor_id_actors_id_fk": { + "name": "agent_runs_requested_by_actor_id_actors_id_fk", + "tableFrom": "agent_runs", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_skill_evaluations": { + "name": "agent_skill_evaluations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_version_id": { + "name": "skill_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "evaluator_actor_id": { + "name": "evaluator_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "suite": { + "name": "suite", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "passed": { + "name": "passed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "baseline_score": { + "name": "baseline_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "regressions": { + "name": "regressions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_skill_evaluations_version_idx": { + "name": "agent_skill_evaluations_version_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_skill_evaluations_organisation_id_organisations_id_fk": { + "name": "agent_skill_evaluations_organisation_id_organisations_id_fk", + "tableFrom": "agent_skill_evaluations", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_evaluations_skill_version_id_agent_skill_versions_id_fk": { + "name": "agent_skill_evaluations_skill_version_id_agent_skill_versions_id_fk", + "tableFrom": "agent_skill_evaluations", + "tableTo": "agent_skill_versions", + "columnsFrom": [ + "skill_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_evaluations_evaluator_actor_id_actors_id_fk": { + "name": "agent_skill_evaluations_evaluator_actor_id_actors_id_fk", + "tableFrom": "agent_skill_evaluations", + "tableTo": "actors", + "columnsFrom": [ + "evaluator_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_skill_evaluations_score_check": { + "name": "agent_skill_evaluations_score_check", + "value": "\"agent_skill_evaluations\".\"score\" between 0 and 100" + } + }, + "isRLSEnabled": false + }, + "public.agent_skill_versions": { + "name": "agent_skill_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "based_on_version_id": { + "name": "based_on_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_rationale": { + "name": "change_rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence_references": { + "name": "evidence_references", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "required_capabilities": { + "name": "required_capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'proposed'" + }, + "approved_by_actor_id": { + "name": "approved_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_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": { + "agent_skill_versions_skill_version_unique": { + "name": "agent_skill_versions_skill_version_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_skill_versions_content_hash_unique": { + "name": "agent_skill_versions_content_hash_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "content_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_skill_versions_organisation_id_organisations_id_fk": { + "name": "agent_skill_versions_organisation_id_organisations_id_fk", + "tableFrom": "agent_skill_versions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_versions_skill_id_agent_skills_id_fk": { + "name": "agent_skill_versions_skill_id_agent_skills_id_fk", + "tableFrom": "agent_skill_versions", + "tableTo": "agent_skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_versions_source_run_id_agent_runs_id_fk": { + "name": "agent_skill_versions_source_run_id_agent_runs_id_fk", + "tableFrom": "agent_skill_versions", + "tableTo": "agent_runs", + "columnsFrom": [ + "source_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_versions_approved_by_actor_id_actors_id_fk": { + "name": "agent_skill_versions_approved_by_actor_id_actors_id_fk", + "tableFrom": "agent_skill_versions", + "tableTo": "actors", + "columnsFrom": [ + "approved_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_skill_versions_state_check": { + "name": "agent_skill_versions_state_check", + "value": "\"agent_skill_versions\".\"state\" in ('proposed','evaluating','approved','rejected','published','rolled_back')" + } + }, + "isRLSEnabled": false + }, + "public.agent_skills": { + "name": "agent_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_key": { + "name": "skill_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "active_version_id": { + "name": "active_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "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": { + "agent_skills_org_agent_key_unique": { + "name": "agent_skills_org_agent_key_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_skills_organisation_id_organisations_id_fk": { + "name": "agent_skills_organisation_id_organisations_id_fk", + "tableFrom": "agent_skills", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skills_agent_id_agent_definitions_id_fk": { + "name": "agent_skills_agent_id_agent_definitions_id_fk", + "tableFrom": "agent_skills", + "tableTo": "agent_definitions", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skills_created_by_actor_id_actors_id_fk": { + "name": "agent_skills_created_by_actor_id_actors_id_fk", + "tableFrom": "agent_skills", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_skills_status_check": { + "name": "agent_skills_status_check", + "value": "\"agent_skills\".\"status\" in ('draft','evaluating','published','retired')" + } + }, + "isRLSEnabled": false + }, + "public.agent_tool_calls": { + "name": "agent_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "arguments_hash": { + "name": "arguments_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result_hash": { + "name": "result_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_tool_calls_org_run_idx": { + "name": "agent_tool_calls_org_run_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_tool_calls_organisation_id_organisations_id_fk": { + "name": "agent_tool_calls_organisation_id_organisations_id_fk", + "tableFrom": "agent_tool_calls", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_tool_calls_run_id_agent_runs_id_fk": { + "name": "agent_tool_calls_run_id_agent_runs_id_fk", + "tableFrom": "agent_tool_calls", + "tableTo": "agent_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_tool_calls_approval_id_approvals_id_fk": { + "name": "agent_tool_calls_approval_id_approvals_id_fk", + "tableFrom": "agent_tool_calls", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alerts": { + "name": "alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_product": { + "name": "source_product", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_instance": { + "name": "source_instance", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_reference": { + "name": "external_reference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "severity": { + "name": "severity", + "type": "severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "alert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "rule_name": { + "name": "rule_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "assigned_actor_id": { + "name": "assigned_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "entities": { + "name": "entities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "observables": { + "name": "observables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "raw_reference_metadata": { + "name": "raw_reference_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kelpie_case_id": { + "name": "kelpie_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_key": { + "name": "correlation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "alerts_org_source_ref_unique": { + "name": "alerts_org_source_ref_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_instance", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_reference", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alerts_org_dedupe_unique": { + "name": "alerts_org_dedupe_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alerts_org_queue_idx": { + "name": "alerts_org_queue_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alerts_search_idx": { + "name": "alerts_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"title\" || ' ' || \"description\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "alerts_organisation_id_organisations_id_fk": { + "name": "alerts_organisation_id_organisations_id_fk", + "tableFrom": "alerts", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "alerts_assigned_actor_id_actors_id_fk": { + "name": "alerts_assigned_actor_id_actors_id_fk", + "tableFrom": "alerts", + "tableTo": "actors", + "columnsFrom": [ + "assigned_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "alerts_room_id_rooms_id_fk": { + "name": "alerts_room_id_rooms_id_fk", + "tableFrom": "alerts", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requesting_actor_id": { + "name": "requesting_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "risk_summary": { + "name": "risk_summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "required_capability": { + "name": "required_capability", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "required_approval_count": { + "name": "required_approval_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "decisions": { + "name": "decisions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "decision_at": { + "name": "decision_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "approvals_org_idempotency_unique": { + "name": "approvals_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approvals_org_status_idx": { + "name": "approvals_org_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_organisation_id_organisations_id_fk": { + "name": "approvals_organisation_id_organisations_id_fk", + "tableFrom": "approvals", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requesting_actor_id_actors_id_fk": { + "name": "approvals_requesting_actor_id_actors_id_fk", + "tableFrom": "approvals", + "tableTo": "actors", + "columnsFrom": [ + "requesting_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_events": { + "name": "audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previous_hash": { + "name": "previous_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_hash": { + "name": "event_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_org_sequence_unique": { + "name": "audit_org_sequence_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_org_hash_unique": { + "name": "audit_org_hash_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_org_target_idx": { + "name": "audit_org_target_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_events_organisation_id_organisations_id_fk": { + "name": "audit_events_organisation_id_organisations_id_fk", + "tableFrom": "audit_events", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "audit_events_actor_id_actors_id_fk": { + "name": "audit_events_actor_id_actors_id_fk", + "tableFrom": "audit_events", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_account": { + "name": "auth_account", + "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": { + "auth_accounts_user_idx": { + "name": "auth_accounts_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_accounts_provider_unique": { + "name": "auth_accounts_provider_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_account_user_id_auth_user_id_fk": { + "name": "auth_account_user_id_auth_user_id_fk", + "tableFrom": "auth_account", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_passkey": { + "name": "auth_passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_passkey_user_idx": { + "name": "auth_passkey_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_passkey_user_id_auth_user_id_fk": { + "name": "auth_passkey_user_id_auth_user_id_fk", + "tableFrom": "auth_passkey", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_passkey_credential_id_unique": { + "name": "auth_passkey_credential_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_session": { + "name": "auth_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "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 + }, + "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": { + "auth_sessions_user_idx": { + "name": "auth_sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_session_user_id_auth_user_id_fk": { + "name": "auth_session_user_id_auth_user_id_fk", + "tableFrom": "auth_session", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_session_token_unique": { + "name": "auth_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_two_factor": { + "name": "auth_two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_two_factor_user_unique": { + "name": "auth_two_factor_user_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_two_factor_user_id_auth_user_id_fk": { + "name": "auth_two_factor_user_id_auth_user_id_fk", + "tableFrom": "auth_two_factor", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_user": { + "name": "auth_user", + "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 + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_user_email_unique": { + "name": "auth_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_verification": { + "name": "auth_verification", + "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": { + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decisions": { + "name": "decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decision_maker_actor_id": { + "name": "decision_maker_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "alternatives_considered": { + "name": "alternatives_considered", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "evidence_references": { + "name": "evidence_references", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "related_investigation_id": { + "name": "related_investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_case_id": { + "name": "related_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_workflow_run_id": { + "name": "related_workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decisions_org_investigation_idx": { + "name": "decisions_org_investigation_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_investigation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decisions_organisation_id_organisations_id_fk": { + "name": "decisions_organisation_id_organisations_id_fk", + "tableFrom": "decisions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_decision_maker_actor_id_actors_id_fk": { + "name": "decisions_decision_maker_actor_id_actors_id_fk", + "tableFrom": "decisions", + "tableTo": "actors", + "columnsFrom": [ + "decision_maker_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_related_investigation_id_investigations_id_fk": { + "name": "decisions_related_investigation_id_investigations_id_fk", + "tableFrom": "decisions", + "tableTo": "investigations", + "columnsFrom": [ + "related_investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.evidence": { + "name": "evidence", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by_actor_id": { + "name": "uploaded_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "related_room_id": { + "name": "related_room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_investigation_id": { + "name": "related_investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_case_id": { + "name": "related_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_timestamp": { + "name": "original_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scan_state": { + "name": "scan_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "retention_state": { + "name": "retention_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "legal_hold": { + "name": "legal_hold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "object_lock_metadata": { + "name": "object_lock_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "evidence_org_hash_unique": { + "name": "evidence_org_hash_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "evidence_search_idx": { + "name": "evidence_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"file_name\" || ' ' || \"source\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "evidence_organisation_id_organisations_id_fk": { + "name": "evidence_organisation_id_organisations_id_fk", + "tableFrom": "evidence", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evidence_uploaded_by_actor_id_actors_id_fk": { + "name": "evidence_uploaded_by_actor_id_actors_id_fk", + "tableFrom": "evidence", + "tableTo": "actors", + "columnsFrom": [ + "uploaded_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evidence_related_room_id_rooms_id_fk": { + "name": "evidence_related_room_id_rooms_id_fk", + "tableFrom": "evidence", + "tableTo": "rooms", + "columnsFrom": [ + "related_room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evidence_related_investigation_id_investigations_id_fk": { + "name": "evidence_related_investigation_id_investigations_id_fk", + "tableFrom": "evidence", + "tableTo": "investigations", + "columnsFrom": [ + "related_investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.findings": { + "name": "findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "supporting_evidence": { + "name": "supporting_evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "related_entities": { + "name": "related_entities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "related_observables": { + "name": "related_observables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recommended_action": { + "name": "recommended_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_provenance": { + "name": "agent_provenance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "human_reviewed_at": { + "name": "human_reviewed_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()" + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "findings_org_investigation_idx": { + "name": "findings_org_investigation_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "investigation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "findings_search_idx": { + "name": "findings_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"title\" || ' ' || \"summary\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "findings_organisation_id_organisations_id_fk": { + "name": "findings_organisation_id_organisations_id_fk", + "tableFrom": "findings", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "findings_investigation_id_investigations_id_fk": { + "name": "findings_investigation_id_investigations_id_fk", + "tableFrom": "findings", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "findings_created_by_actor_id_actors_id_fk": { + "name": "findings_created_by_actor_id_actors_id_fk", + "tableFrom": "findings", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hunt_queries": { + "name": "hunt_queries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "hunt_id": { + "name": "hunt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "query_run_id": { + "name": "query_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "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": { + "hunt_queries_org_query_run_unique": { + "name": "hunt_queries_org_query_run_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "query_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hunt_queries_org_hunt_sequence_unique": { + "name": "hunt_queries_org_hunt_sequence_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hunt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hunt_queries_org_hunt_idx": { + "name": "hunt_queries_org_hunt_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hunt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hunt_queries_organisation_id_organisations_id_fk": { + "name": "hunt_queries_organisation_id_organisations_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_queries_hunt_id_hunt_runs_id_fk": { + "name": "hunt_queries_hunt_id_hunt_runs_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "hunt_runs", + "columnsFrom": [ + "hunt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_queries_integration_id_integration_records_id_fk": { + "name": "hunt_queries_integration_id_integration_records_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_queries_template_id_integration_query_templates_id_fk": { + "name": "hunt_queries_template_id_integration_query_templates_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "integration_query_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_queries_query_run_id_integration_query_runs_id_fk": { + "name": "hunt_queries_query_run_id_integration_query_runs_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "integration_query_runs", + "columnsFrom": [ + "query_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hunt_runs": { + "name": "hunt_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_message_id": { + "name": "source_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_case_id": { + "name": "linked_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "training_mode": { + "name": "training_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "plan": { + "name": "plan", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hunt_runs_org_idempotency_unique": { + "name": "hunt_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hunt_runs_org_agent_run_unique": { + "name": "hunt_runs_org_agent_run_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hunt_runs_org_status_idx": { + "name": "hunt_runs_org_status_idx", + "columns": [ + { + "expression": "organisation_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": { + "hunt_runs_organisation_id_organisations_id_fk": { + "name": "hunt_runs_organisation_id_organisations_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_agent_run_id_agent_runs_id_fk": { + "name": "hunt_runs_agent_run_id_agent_runs_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "agent_runs", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_task_id_tasks_id_fk": { + "name": "hunt_runs_task_id_tasks_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_source_message_id_messages_id_fk": { + "name": "hunt_runs_source_message_id_messages_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "messages", + "columnsFrom": [ + "source_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_room_id_rooms_id_fk": { + "name": "hunt_runs_room_id_rooms_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_requested_by_actor_id_actors_id_fk": { + "name": "hunt_runs_requested_by_actor_id_actors_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_approval_id_approvals_id_fk": { + "name": "hunt_runs_approval_id_approvals_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "hunt_runs_status_check": { + "name": "hunt_runs_status_check", + "value": "\"hunt_runs\".\"status\" in ('planned','awaiting_approval','querying','analysing','completed','failed','cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.hypotheses": { + "name": "hypotheses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "statement": { + "name": "statement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unverified'" + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "supporting_finding_ids": { + "name": "supporting_finding_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "contradicting_finding_ids": { + "name": "contradicting_finding_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "hypotheses_org_investigation_idx": { + "name": "hypotheses_org_investigation_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "investigation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hypotheses_organisation_id_organisations_id_fk": { + "name": "hypotheses_organisation_id_organisations_id_fk", + "tableFrom": "hypotheses", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hypotheses_investigation_id_investigations_id_fk": { + "name": "hypotheses_investigation_id_investigations_id_fk", + "tableFrom": "hypotheses", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hypotheses_created_by_actor_id_actors_id_fk": { + "name": "hypotheses_created_by_actor_id_actors_id_fk", + "tableFrom": "hypotheses", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_records": { + "name": "idempotency_records", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idempotency_expiry_idx": { + "name": "idempotency_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "idempotency_records_organisation_id_organisations_id_fk": { + "name": "idempotency_records_organisation_id_organisations_id_fk", + "tableFrom": "idempotency_records", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "idempotency_records_organisation_id_scope_key_pk": { + "name": "idempotency_records_organisation_id_scope_key_pk", + "columns": [ + "organisation_id", + "scope", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connector_credentials": { + "name": "integration_connector_credentials", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "encrypted_credential": { + "name": "encrypted_credential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "envelope_version": { + "name": "envelope_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'v1'" + }, + "rotation_version": { + "name": "rotation_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "rotated_by_actor_id": { + "name": "rotated_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_credentials_org_idx": { + "name": "integration_credentials_org_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_connector_credentials_organisation_id_organisations_id_fk": { + "name": "integration_connector_credentials_organisation_id_organisations_id_fk", + "tableFrom": "integration_connector_credentials", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_connector_credentials_integration_id_integration_records_id_fk": { + "name": "integration_connector_credentials_integration_id_integration_records_id_fk", + "tableFrom": "integration_connector_credentials", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_connector_credentials_rotated_by_actor_id_actors_id_fk": { + "name": "integration_connector_credentials_rotated_by_actor_id_actors_id_fk", + "tableFrom": "integration_connector_credentials", + "tableTo": "actors", + "columnsFrom": [ + "rotated_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_deliveries": { + "name": "integration_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_metadata": { + "name": "request_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "response_metadata": { + "name": "response_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_delivery_org_idempotency_unique": { + "name": "integration_delivery_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_deliveries_organisation_id_organisations_id_fk": { + "name": "integration_deliveries_organisation_id_organisations_id_fk", + "tableFrom": "integration_deliveries", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_deliveries_integration_id_integration_records_id_fk": { + "name": "integration_deliveries_integration_id_integration_records_id_fk", + "tableFrom": "integration_deliveries", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_entities": { + "name": "integration_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "posture": { + "name": "posture", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_entities_org_external_unique": { + "name": "integration_entities_org_external_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_entities_organisation_id_organisations_id_fk": { + "name": "integration_entities_organisation_id_organisations_id_fk", + "tableFrom": "integration_entities", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_entities_integration_id_integration_records_id_fk": { + "name": "integration_entities_integration_id_integration_records_id_fk", + "tableFrom": "integration_entities", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_query_runs": { + "name": "integration_query_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "request_metadata": { + "name": "request_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "response_metadata": { + "name": "response_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_query_runs_org_idempotency_unique": { + "name": "integration_query_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_query_runs_org_status_idx": { + "name": "integration_query_runs_org_status_idx", + "columns": [ + { + "expression": "organisation_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": { + "integration_query_runs_organisation_id_organisations_id_fk": { + "name": "integration_query_runs_organisation_id_organisations_id_fk", + "tableFrom": "integration_query_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_runs_integration_id_integration_records_id_fk": { + "name": "integration_query_runs_integration_id_integration_records_id_fk", + "tableFrom": "integration_query_runs", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_runs_template_id_integration_query_templates_id_fk": { + "name": "integration_query_runs_template_id_integration_query_templates_id_fk", + "tableFrom": "integration_query_runs", + "tableTo": "integration_query_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_runs_requested_by_actor_id_actors_id_fk": { + "name": "integration_query_runs_requested_by_actor_id_actors_id_fk", + "tableFrom": "integration_query_runs", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_query_templates": { + "name": "integration_query_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_key": { + "name": "template_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "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": { + "integration_templates_org_key_version_unique": { + "name": "integration_templates_org_key_version_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_query_templates_organisation_id_organisations_id_fk": { + "name": "integration_query_templates_organisation_id_organisations_id_fk", + "tableFrom": "integration_query_templates", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_templates_integration_id_integration_records_id_fk": { + "name": "integration_query_templates_integration_id_integration_records_id_fk", + "tableFrom": "integration_query_templates", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_templates_created_by_actor_id_actors_id_fk": { + "name": "integration_query_templates_created_by_actor_id_actors_id_fk", + "tableFrom": "integration_query_templates", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_records": { + "name": "integration_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mock": { + "name": "mock", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "configuration": { + "name": "configuration", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "health": { + "name": "health", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cursor": { + "name": "cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integrations_org_product_instance_unique": { + "name": "integrations_org_product_instance_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_records_organisation_id_organisations_id_fk": { + "name": "integration_records_organisation_id_organisations_id_fk", + "tableFrom": "integration_records", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.investigations": { + "name": "investigations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "investigation_number": { + "name": "investigation_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "investigation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "severity": { + "name": "severity", + "type": "severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "lead_actor_id": { + "name": "lead_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recommendation": { + "name": "recommendation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promotion_decision": { + "name": "promotion_decision", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "linked_kelpie_case_id": { + "name": "linked_kelpie_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "investigations_org_number_unique": { + "name": "investigations_org_number_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "investigation_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "investigations_org_queue_idx": { + "name": "investigations_org_queue_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_activity_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "investigations_search_idx": { + "name": "investigations_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"title\" || ' ' || \"summary\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "investigations_organisation_id_organisations_id_fk": { + "name": "investigations_organisation_id_organisations_id_fk", + "tableFrom": "investigations", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "investigations_lead_actor_id_actors_id_fk": { + "name": "investigations_lead_actor_id_actors_id_fk", + "tableFrom": "investigations", + "tableTo": "actors", + "columnsFrom": [ + "lead_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "investigations_room_id_rooms_id_fk": { + "name": "investigations_room_id_rooms_id_fk", + "tableFrom": "investigations", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_mentions": { + "name": "message_mentions", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mentioned_actor_id": { + "name": "mentioned_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mention_type": { + "name": "mention_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mention_key": { + "name": "mention_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_mentions_org_actor_idx": { + "name": "message_mentions_org_actor_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mentioned_actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_mentions_organisation_id_organisations_id_fk": { + "name": "message_mentions_organisation_id_organisations_id_fk", + "tableFrom": "message_mentions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_mentions_message_id_messages_id_fk": { + "name": "message_mentions_message_id_messages_id_fk", + "tableFrom": "message_mentions", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_mentions_mentioned_actor_id_actors_id_fk": { + "name": "message_mentions_mentioned_actor_id_actors_id_fk", + "tableFrom": "message_mentions", + "tableTo": "actors", + "columnsFrom": [ + "mentioned_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_mentions_message_id_mention_type_mention_key_pk": { + "name": "message_mentions_message_id_mention_type_mention_key_pk", + "columns": [ + "message_id", + "mention_type", + "mention_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "message_mention_type_check": { + "name": "message_mention_type_check", + "value": "\"message_mentions\".\"mention_type\" in ('actor','room','everyone')" + } + }, + "isRLSEnabled": false + }, + "public.message_pins": { + "name": "message_pins", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pinned_by_actor_id": { + "name": "pinned_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_pins_org_room_idx": { + "name": "message_pins_org_room_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_pins_organisation_id_organisations_id_fk": { + "name": "message_pins_organisation_id_organisations_id_fk", + "tableFrom": "message_pins", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_pins_room_id_rooms_id_fk": { + "name": "message_pins_room_id_rooms_id_fk", + "tableFrom": "message_pins", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_pins_message_id_messages_id_fk": { + "name": "message_pins_message_id_messages_id_fk", + "tableFrom": "message_pins", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_pins_pinned_by_actor_id_actors_id_fk": { + "name": "message_pins_pinned_by_actor_id_actors_id_fk", + "tableFrom": "message_pins", + "tableTo": "actors", + "columnsFrom": [ + "pinned_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_pins_room_id_message_id_pk": { + "name": "message_pins_room_id_message_id_pk", + "columns": [ + "room_id", + "message_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_revisions": { + "name": "message_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_type": { + "name": "revision_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previous_document": { + "name": "previous_document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "previous_plain_text": { + "name": "previous_plain_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "next_document": { + "name": "next_document", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_plain_text": { + "name": "next_plain_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_revisions_org_message_idx": { + "name": "message_revisions_org_message_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_revisions_org_idempotency_unique": { + "name": "message_revisions_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_revisions_organisation_id_organisations_id_fk": { + "name": "message_revisions_organisation_id_organisations_id_fk", + "tableFrom": "message_revisions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_revisions_message_id_messages_id_fk": { + "name": "message_revisions_message_id_messages_id_fk", + "tableFrom": "message_revisions", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_revisions_actor_id_actors_id_fk": { + "name": "message_revisions_actor_id_actors_id_fk", + "tableFrom": "message_revisions", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "message_revision_type_check": { + "name": "message_revision_type_check", + "value": "\"message_revisions\".\"revision_type\" in ('edit','delete')" + } + }, + "isRLSEnabled": false + }, + "public.message_saves": { + "name": "message_saves", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_saves_org_actor_idx": { + "name": "message_saves_org_actor_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_saves_organisation_id_organisations_id_fk": { + "name": "message_saves_organisation_id_organisations_id_fk", + "tableFrom": "message_saves", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_saves_message_id_messages_id_fk": { + "name": "message_saves_message_id_messages_id_fk", + "tableFrom": "message_saves", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_saves_actor_id_actors_id_fk": { + "name": "message_saves_actor_id_actors_id_fk", + "tableFrom": "message_saves", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_saves_message_id_actor_id_pk": { + "name": "message_saves_message_id_actor_id_pk", + "columns": [ + "message_id", + "actor_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "thread_parent_id": { + "name": "thread_parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_actor_id": { + "name": "author_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_type": { + "name": "message_type", + "type": "message_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "plain_text": { + "name": "plain_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "edited_at": { + "name": "edited_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "data_classification": { + "name": "data_classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "related_alert_id": { + "name": "related_alert_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_investigation_id": { + "name": "related_investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_case_id": { + "name": "related_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_agent_run_id": { + "name": "related_agent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_workflow_run_id": { + "name": "related_workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "messages_org_room_time_idx": { + "name": "messages_org_room_time_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_thread_idx": { + "name": "messages_thread_idx", + "columns": [ + { + "expression": "thread_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_org_idempotency_unique": { + "name": "messages_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"messages\".\"idempotency_key\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_search_idx": { + "name": "messages_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"plain_text\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "messages_organisation_id_organisations_id_fk": { + "name": "messages_organisation_id_organisations_id_fk", + "tableFrom": "messages", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "messages_room_id_rooms_id_fk": { + "name": "messages_room_id_rooms_id_fk", + "tableFrom": "messages", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "messages_author_actor_id_actors_id_fk": { + "name": "messages_author_actor_id_actors_id_fk", + "tableFrom": "messages", + "tableTo": "actors", + "columnsFrom": [ + "author_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "messages_related_alert_id_alerts_id_fk": { + "name": "messages_related_alert_id_alerts_id_fk", + "tableFrom": "messages", + "tableTo": "alerts", + "columnsFrom": [ + "related_alert_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "messages_related_investigation_id_investigations_id_fk": { + "name": "messages_related_investigation_id_investigations_id_fk", + "tableFrom": "messages", + "tableTo": "investigations", + "columnsFrom": [ + "related_investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "safe_preview": { + "name": "safe_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_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": { + "notifications_org_actor_read_idx": { + "name": "notifications_org_actor_read_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_organisation_id_organisations_id_fk": { + "name": "notifications_organisation_id_organisations_id_fk", + "tableFrom": "notifications", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notifications_actor_id_actors_id_fk": { + "name": "notifications_actor_id_actors_id_fk", + "tableFrom": "notifications", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organisations": { + "name": "organisations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data_region": { + "name": "data_region", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'australia'" + }, + "default_timezone": { + "name": "default_timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Australia/Sydney'" + }, + "retention_policy": { + "name": "retention_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "authentication_policy": { + "name": "authentication_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organisations_slug_unique": { + "name": "organisations_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organisations_status_check": { + "name": "organisations_status_check", + "value": "\"organisations\".\"status\" in ('active','suspended')" + } + }, + "isRLSEnabled": false + }, + "public.outbox_events": { + "name": "outbox_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aggregate_type": { + "name": "aggregate_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aggregate_id": { + "name": "aggregate_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queue_name": { + "name": "queue_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "outbox_idempotency_unique": { + "name": "outbox_idempotency_unique", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_pending_idx": { + "name": "outbox_pending_idx", + "columns": [ + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"outbox_events\".\"dispatched_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "outbox_events_organisation_id_organisations_id_fk": { + "name": "outbox_events_organisation_id_organisations_id_fk", + "tableFrom": "outbox_events", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reaction_operations": { + "name": "reaction_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reaction_operations_org_idempotency_unique": { + "name": "reaction_operations_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_operations_org_message_idx": { + "name": "reaction_operations_org_message_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_operations_organisation_id_organisations_id_fk": { + "name": "reaction_operations_organisation_id_organisations_id_fk", + "tableFrom": "reaction_operations", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_operations_message_id_messages_id_fk": { + "name": "reaction_operations_message_id_messages_id_fk", + "tableFrom": "reaction_operations", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_operations_actor_id_actors_id_fk": { + "name": "reaction_operations_actor_id_actors_id_fk", + "tableFrom": "reaction_operations", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reaction_pack_assets": { + "name": "reaction_pack_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_id": { + "name": "revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "frame_count": { + "name": "frame_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_state": { + "name": "verification_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'verified'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reaction_pack_assets_org_revision_name_unique": { + "name": "reaction_pack_assets_org_revision_name_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_pack_assets_org_digest_idx": { + "name": "reaction_pack_assets_org_digest_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_pack_assets_organisation_id_organisations_id_fk": { + "name": "reaction_pack_assets_organisation_id_organisations_id_fk", + "tableFrom": "reaction_pack_assets", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_pack_assets_revision_id_reaction_pack_revisions_id_fk": { + "name": "reaction_pack_assets_revision_id_reaction_pack_revisions_id_fk", + "tableFrom": "reaction_pack_assets", + "tableTo": "reaction_pack_revisions", + "columnsFrom": [ + "revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reaction_pack_assets_verification_check": { + "name": "reaction_pack_assets_verification_check", + "value": "\"reaction_pack_assets\".\"verification_state\" in ('verified','missing','mismatch')" + }, + "reaction_pack_assets_dimensions_check": { + "name": "reaction_pack_assets_dimensions_check", + "value": "\"reaction_pack_assets\".\"width\" > 0 and \"reaction_pack_assets\".\"height\" > 0 and \"reaction_pack_assets\".\"frame_count\" > 0 and \"reaction_pack_assets\".\"byte_size\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.reaction_pack_revisions": { + "name": "reaction_pack_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_actor_id": { + "name": "approved_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "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": { + "reaction_pack_revisions_org_pack_revision_unique": { + "name": "reaction_pack_revisions_org_pack_revision_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pack_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_pack_revisions_org_status_idx": { + "name": "reaction_pack_revisions_org_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_pack_revisions_organisation_id_organisations_id_fk": { + "name": "reaction_pack_revisions_organisation_id_organisations_id_fk", + "tableFrom": "reaction_pack_revisions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_pack_revisions_pack_id_reaction_packs_id_fk": { + "name": "reaction_pack_revisions_pack_id_reaction_packs_id_fk", + "tableFrom": "reaction_pack_revisions", + "tableTo": "reaction_packs", + "columnsFrom": [ + "pack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_pack_revisions_approved_by_actor_id_actors_id_fk": { + "name": "reaction_pack_revisions_approved_by_actor_id_actors_id_fk", + "tableFrom": "reaction_pack_revisions", + "tableTo": "actors", + "columnsFrom": [ + "approved_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_pack_revisions_created_by_actor_id_actors_id_fk": { + "name": "reaction_pack_revisions_created_by_actor_id_actors_id_fk", + "tableFrom": "reaction_pack_revisions", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reaction_pack_revisions_status_check": { + "name": "reaction_pack_revisions_status_check", + "value": "\"reaction_pack_revisions\".\"status\" in ('draft','approved','superseded','removed')" + }, + "reaction_pack_revisions_revision_check": { + "name": "reaction_pack_revisions_revision_check", + "value": "\"reaction_pack_revisions\".\"revision\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.reaction_packs": { + "name": "reaction_packs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "removed_by_actor_id": { + "name": "removed_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reaction_packs_org_slug_unique": { + "name": "reaction_packs_org_slug_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_packs_org_lifecycle_idx": { + "name": "reaction_packs_org_lifecycle_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lifecycle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_packs_organisation_id_organisations_id_fk": { + "name": "reaction_packs_organisation_id_organisations_id_fk", + "tableFrom": "reaction_packs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_packs_created_by_actor_id_actors_id_fk": { + "name": "reaction_packs_created_by_actor_id_actors_id_fk", + "tableFrom": "reaction_packs", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_packs_removed_by_actor_id_actors_id_fk": { + "name": "reaction_packs_removed_by_actor_id_actors_id_fk", + "tableFrom": "reaction_packs", + "tableTo": "actors", + "columnsFrom": [ + "removed_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reaction_packs_lifecycle_check": { + "name": "reaction_packs_lifecycle_check", + "value": "\"reaction_packs\".\"lifecycle\" in ('active','removed')" + } + }, + "isRLSEnabled": false + }, + "public.reactions": { + "name": "reactions", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reactions_organisation_id_organisations_id_fk": { + "name": "reactions_organisation_id_organisations_id_fk", + "tableFrom": "reactions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reactions_message_id_messages_id_fk": { + "name": "reactions_message_id_messages_id_fk", + "tableFrom": "reactions", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reactions_actor_id_actors_id_fk": { + "name": "reactions_actor_id_actors_id_fk", + "tableFrom": "reactions", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reactions_message_id_actor_id_emoji_pk": { + "name": "reactions_message_id_actor_id_emoji_pk", + "columns": [ + "message_id", + "actor_id", + "emoji" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.report_deliveries": { + "name": "report_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "report_id": { + "name": "report_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient": { + "name": "recipient", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'awaiting_approval'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "report_deliveries_org_idempotency_unique": { + "name": "report_deliveries_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "report_deliveries_org_report_status_idx": { + "name": "report_deliveries_org_report_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "report_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "report_deliveries_organisation_id_organisations_id_fk": { + "name": "report_deliveries_organisation_id_organisations_id_fk", + "tableFrom": "report_deliveries", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_deliveries_report_id_report_manifests_id_fk": { + "name": "report_deliveries_report_id_report_manifests_id_fk", + "tableFrom": "report_deliveries", + "tableTo": "report_manifests", + "columnsFrom": [ + "report_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_deliveries_approval_id_approvals_id_fk": { + "name": "report_deliveries_approval_id_approvals_id_fk", + "tableFrom": "report_deliveries", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_deliveries_requested_by_actor_id_actors_id_fk": { + "name": "report_deliveries_requested_by_actor_id_actors_id_fk", + "tableFrom": "report_deliveries", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "report_deliveries_status_check": { + "name": "report_deliveries_status_check", + "value": "\"report_deliveries\".\"status\" in ('awaiting_approval','queued','delivered','failed','cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.report_manifests": { + "name": "report_manifests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "review_note": { + "name": "review_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "posted_message_id": { + "name": "posted_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "report_manifests_org_idempotency_unique": { + "name": "report_manifests_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "report_manifests_org_room_status_idx": { + "name": "report_manifests_org_room_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_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": { + "report_manifests_organisation_id_organisations_id_fk": { + "name": "report_manifests_organisation_id_organisations_id_fk", + "tableFrom": "report_manifests", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_agent_run_id_agent_runs_id_fk": { + "name": "report_manifests_agent_run_id_agent_runs_id_fk", + "tableFrom": "report_manifests", + "tableTo": "agent_runs", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_task_id_tasks_id_fk": { + "name": "report_manifests_task_id_tasks_id_fk", + "tableFrom": "report_manifests", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_room_id_rooms_id_fk": { + "name": "report_manifests_room_id_rooms_id_fk", + "tableFrom": "report_manifests", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_requested_by_actor_id_actors_id_fk": { + "name": "report_manifests_requested_by_actor_id_actors_id_fk", + "tableFrom": "report_manifests", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_posted_message_id_messages_id_fk": { + "name": "report_manifests_posted_message_id_messages_id_fk", + "tableFrom": "report_manifests", + "tableTo": "messages", + "columnsFrom": [ + "posted_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "report_manifests_status_check": { + "name": "report_manifests_status_check", + "value": "\"report_manifests\".\"status\" in ('draft','reviewed','posted','superseded')" + }, + "report_manifests_version_check": { + "name": "report_manifests_version_check", + "value": "\"report_manifests\".\"version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.report_schedules": { + "name": "report_schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'leadership'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "report_schedules_org_idempotency_unique": { + "name": "report_schedules_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "report_schedules_org_due_idx": { + "name": "report_schedules_org_due_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "report_schedules_organisation_id_organisations_id_fk": { + "name": "report_schedules_organisation_id_organisations_id_fk", + "tableFrom": "report_schedules", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_schedules_room_id_rooms_id_fk": { + "name": "report_schedules_room_id_rooms_id_fk", + "tableFrom": "report_schedules", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_schedules_created_by_actor_id_actors_id_fk": { + "name": "report_schedules_created_by_actor_id_actors_id_fk", + "tableFrom": "report_schedules", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "report_schedules_cadence_check": { + "name": "report_schedules_cadence_check", + "value": "\"report_schedules\".\"cadence\" in ('weekly','monthly')" + }, + "report_schedules_audience_check": { + "name": "report_schedules_audience_check", + "value": "\"report_schedules\".\"audience\" in ('analyst','leadership','executive')" + } + }, + "isRLSEnabled": false + }, + "public.research_items": { + "name": "research_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "watchlist_id": { + "name": "watchlist_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "research_run_id": { + "name": "research_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_published_at": { + "name": "source_published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "root_message_id": { + "name": "root_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_message_id": { + "name": "latest_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "brief": { + "name": "brief", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_by_actor_id": { + "name": "feedback_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "feedback_at": { + "name": "feedback_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "research_items_org_fingerprint_unique": { + "name": "research_items_org_fingerprint_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_items_org_watchlist_idx": { + "name": "research_items_org_watchlist_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "watchlist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "research_items_organisation_id_organisations_id_fk": { + "name": "research_items_organisation_id_organisations_id_fk", + "tableFrom": "research_items", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_watchlist_id_research_watchlists_id_fk": { + "name": "research_items_watchlist_id_research_watchlists_id_fk", + "tableFrom": "research_items", + "tableTo": "research_watchlists", + "columnsFrom": [ + "watchlist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_research_run_id_research_runs_id_fk": { + "name": "research_items_research_run_id_research_runs_id_fk", + "tableFrom": "research_items", + "tableTo": "research_runs", + "columnsFrom": [ + "research_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_root_message_id_messages_id_fk": { + "name": "research_items_root_message_id_messages_id_fk", + "tableFrom": "research_items", + "tableTo": "messages", + "columnsFrom": [ + "root_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_latest_message_id_messages_id_fk": { + "name": "research_items_latest_message_id_messages_id_fk", + "tableFrom": "research_items", + "tableTo": "messages", + "columnsFrom": [ + "latest_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_feedback_by_actor_id_actors_id_fk": { + "name": "research_items_feedback_by_actor_id_actors_id_fk", + "tableFrom": "research_items", + "tableTo": "actors", + "columnsFrom": [ + "feedback_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.research_runs": { + "name": "research_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "watchlist_id": { + "name": "watchlist_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "source_limit": { + "name": "source_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_budget": { + "name": "token_budget", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost_limit_cents": { + "name": "cost_limit_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "time_limit_seconds": { + "name": "time_limit_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "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": { + "research_runs_org_idempotency_unique": { + "name": "research_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_runs_org_agent_unique": { + "name": "research_runs_org_agent_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_runs_org_status_idx": { + "name": "research_runs_org_status_idx", + "columns": [ + { + "expression": "organisation_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": { + "research_runs_organisation_id_organisations_id_fk": { + "name": "research_runs_organisation_id_organisations_id_fk", + "tableFrom": "research_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_runs_watchlist_id_research_watchlists_id_fk": { + "name": "research_runs_watchlist_id_research_watchlists_id_fk", + "tableFrom": "research_runs", + "tableTo": "research_watchlists", + "columnsFrom": [ + "watchlist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_runs_agent_run_id_agent_runs_id_fk": { + "name": "research_runs_agent_run_id_agent_runs_id_fk", + "tableFrom": "research_runs", + "tableTo": "agent_runs", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "research_runs_status_check": { + "name": "research_runs_status_check", + "value": "\"research_runs\".\"status\" in ('queued','running','completed','failed')" + } + }, + "isRLSEnabled": false + }, + "public.research_watchlists": { + "name": "research_watchlists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vendors": { + "name": "vendors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "technologies": { + "name": "technologies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cadence_minutes": { + "name": "cadence_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 240 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "research_watchlists_org_name_unique": { + "name": "research_watchlists_org_name_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_watchlists_due_idx": { + "name": "research_watchlists_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "research_watchlists_organisation_id_organisations_id_fk": { + "name": "research_watchlists_organisation_id_organisations_id_fk", + "tableFrom": "research_watchlists", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_watchlists_room_id_rooms_id_fk": { + "name": "research_watchlists_room_id_rooms_id_fk", + "tableFrom": "research_watchlists", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_watchlists_created_by_actor_id_actors_id_fk": { + "name": "research_watchlists_created_by_actor_id_actors_id_fk", + "tableFrom": "research_watchlists", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "research_watchlists_cadence_check": { + "name": "research_watchlists_cadence_check", + "value": "\"research_watchlists\".\"cadence_minutes\" between 15 and 10080" + } + }, + "isRLSEnabled": false + }, + "public.room_integration_bindings": { + "name": "room_integration_bindings", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "room_integration_bindings_org_room_idx": { + "name": "room_integration_bindings_org_room_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "room_integration_bindings_organisation_id_organisations_id_fk": { + "name": "room_integration_bindings_organisation_id_organisations_id_fk", + "tableFrom": "room_integration_bindings", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_integration_bindings_room_id_rooms_id_fk": { + "name": "room_integration_bindings_room_id_rooms_id_fk", + "tableFrom": "room_integration_bindings", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_integration_bindings_integration_id_integration_records_id_fk": { + "name": "room_integration_bindings_integration_id_integration_records_id_fk", + "tableFrom": "room_integration_bindings", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_integration_bindings_created_by_actor_id_actors_id_fk": { + "name": "room_integration_bindings_created_by_actor_id_actors_id_fk", + "tableFrom": "room_integration_bindings", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "room_integration_bindings_room_id_integration_id_pk": { + "name": "room_integration_bindings_room_id_integration_id_pk", + "columns": [ + "room_id", + "integration_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.room_invitations": { + "name": "room_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invited_actor_id": { + "name": "invited_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_expires_at": { + "name": "access_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by_actor_id": { + "name": "invited_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "responded_at": { + "name": "responded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "room_invitations_org_idempotency_unique": { + "name": "room_invitations_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "room_invitations_org_room_status_idx": { + "name": "room_invitations_org_room_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "room_invitations_organisation_id_organisations_id_fk": { + "name": "room_invitations_organisation_id_organisations_id_fk", + "tableFrom": "room_invitations", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_invitations_room_id_rooms_id_fk": { + "name": "room_invitations_room_id_rooms_id_fk", + "tableFrom": "room_invitations", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_invitations_invited_actor_id_actors_id_fk": { + "name": "room_invitations_invited_actor_id_actors_id_fk", + "tableFrom": "room_invitations", + "tableTo": "actors", + "columnsFrom": [ + "invited_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_invitations_invited_by_actor_id_actors_id_fk": { + "name": "room_invitations_invited_by_actor_id_actors_id_fk", + "tableFrom": "room_invitations", + "tableTo": "actors", + "columnsFrom": [ + "invited_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "room_invitation_role_check": { + "name": "room_invitation_role_check", + "value": "\"room_invitations\".\"membership_role\" in ('moderator','member','guest','agent_member')" + }, + "room_invitation_status_check": { + "name": "room_invitation_status_check", + "value": "\"room_invitations\".\"status\" in ('pending','accepted','revoked','expired')" + } + }, + "isRLSEnabled": false + }, + "public.room_memberships": { + "name": "room_memberships", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_level": { + "name": "notification_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "notify_replies": { + "name": "notify_replies", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_followed_threads": { + "name": "notify_followed_threads", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_read_event_id": { + "name": "last_read_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "muted": { + "name": "muted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "favourite": { + "name": "favourite", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sidebar_position": { + "name": "sidebar_position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sidebar_group": { + "name": "sidebar_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_expires_at": { + "name": "access_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "room_memberships_org_actor_idx": { + "name": "room_memberships_org_actor_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "room_memberships_organisation_id_organisations_id_fk": { + "name": "room_memberships_organisation_id_organisations_id_fk", + "tableFrom": "room_memberships", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_memberships_room_id_rooms_id_fk": { + "name": "room_memberships_room_id_rooms_id_fk", + "tableFrom": "room_memberships", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_memberships_actor_id_actors_id_fk": { + "name": "room_memberships_actor_id_actors_id_fk", + "tableFrom": "room_memberships", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "room_memberships_room_id_actor_id_pk": { + "name": "room_memberships_room_id_actor_id_pk", + "columns": [ + "room_id", + "actor_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "room_membership_role_check": { + "name": "room_membership_role_check", + "value": "\"room_memberships\".\"membership_role\" in ('owner','moderator','member','guest','agent_member')" + } + }, + "isRLSEnabled": false + }, + "public.rooms": { + "name": "rooms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "room_type": { + "name": "room_type", + "type": "room_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'organisation'" + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "policies": { + "name": "policies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "direct_fingerprint": { + "name": "direct_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "linked_investigation_id": { + "name": "linked_investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_kelpie_case_id": { + "name": "linked_kelpie_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_severity": { + "name": "default_severity", + "type": "severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "tlp": { + "name": "tlp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'amber'" + }, + "retention_policy": { + "name": "retention_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "rooms_org_slug_unique": { + "name": "rooms_org_slug_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rooms_org_direct_fingerprint_unique": { + "name": "rooms_org_direct_fingerprint_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direct_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"rooms\".\"direct_fingerprint\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "rooms_org_type_idx": { + "name": "rooms_org_type_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rooms_organisation_id_organisations_id_fk": { + "name": "rooms_organisation_id_organisations_id_fk", + "tableFrom": "rooms", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rooms_created_by_actor_id_actors_id_fk": { + "name": "rooms_created_by_actor_id_actors_id_fk", + "tableFrom": "rooms", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "priority": { + "name": "priority", + "type": "task_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "assigned_actor_id": { + "name": "assigned_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_case_id": { + "name": "related_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approval_required": { + "name": "approval_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_run_status": { + "name": "agent_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_org_status_idx": { + "name": "tasks_org_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_org_assignee_idx": { + "name": "tasks_org_assignee_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assigned_actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_org_idempotency_unique": { + "name": "tasks_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_organisation_id_organisations_id_fk": { + "name": "tasks_organisation_id_organisations_id_fk", + "tableFrom": "tasks", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_assigned_actor_id_actors_id_fk": { + "name": "tasks_assigned_actor_id_actors_id_fk", + "tableFrom": "tasks", + "tableTo": "actors", + "columnsFrom": [ + "assigned_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_created_by_actor_id_actors_id_fk": { + "name": "tasks_created_by_actor_id_actors_id_fk", + "tableFrom": "tasks", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_room_id_rooms_id_fk": { + "name": "tasks_room_id_rooms_id_fk", + "tableFrom": "tasks", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_investigation_id_investigations_id_fk": { + "name": "tasks_investigation_id_investigations_id_fk", + "tableFrom": "tasks", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.thread_follows": { + "name": "thread_follows", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "root_message_id": { + "name": "root_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "thread_follows_org_actor_idx": { + "name": "thread_follows_org_actor_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "thread_follows_organisation_id_organisations_id_fk": { + "name": "thread_follows_organisation_id_organisations_id_fk", + "tableFrom": "thread_follows", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "thread_follows_root_message_id_messages_id_fk": { + "name": "thread_follows_root_message_id_messages_id_fk", + "tableFrom": "thread_follows", + "tableTo": "messages", + "columnsFrom": [ + "root_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "thread_follows_actor_id_actors_id_fk": { + "name": "thread_follows_actor_id_actors_id_fk", + "tableFrom": "thread_follows", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "thread_follows_root_message_id_actor_id_pk": { + "name": "thread_follows_root_message_id_actor_id_pk", + "columns": [ + "root_message_id", + "actor_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.timeline_events": { + "name": "timeline_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "external_case_id": { + "name": "external_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "occurred_at": { + "name": "occurred_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()" + } + }, + "indexes": { + "timeline_org_investigation_time_idx": { + "name": "timeline_org_investigation_time_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "investigation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "timeline_events_organisation_id_organisations_id_fk": { + "name": "timeline_events_organisation_id_organisations_id_fk", + "tableFrom": "timeline_events", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "timeline_events_investigation_id_investigations_id_fk": { + "name": "timeline_events_investigation_id_investigations_id_fk", + "tableFrom": "timeline_events", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "timeline_events_room_id_rooms_id_fk": { + "name": "timeline_events_room_id_rooms_id_fk", + "tableFrom": "timeline_events", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "timeline_events_actor_id_actors_id_fk": { + "name": "timeline_events_actor_id_actors_id_fk", + "tableFrom": "timeline_events", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "better_auth_user_id": { + "name": "better_auth_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team": { + "name": "team", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "presence_state": { + "name": "presence_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'offline'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Australia/Sydney'" + }, + "notification_preferences": { + "name": "notification_preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_org_email_unique": { + "name": "users_org_email_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_better_auth_unique": { + "name": "users_better_auth_unique", + "columns": [ + { + "expression": "better_auth_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_org_idx": { + "name": "users_org_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_organisation_id_organisations_id_fk": { + "name": "users_organisation_id_organisations_id_fk", + "tableFrom": "users", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_better_auth_user_id_auth_user_id_fk": { + "name": "users_better_auth_user_id_auth_user_id_fk", + "tableFrom": "users", + "tableTo": "auth_user", + "columnsFrom": [ + "better_auth_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_definitions": { + "name": "workflow_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_key": { + "name": "workflow_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "yaml": { + "name": "yaml", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parsed": { + "name": "parsed", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "owner_actor_id": { + "name": "owner_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "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": { + "workflow_defs_org_key_version_unique": { + "name": "workflow_defs_org_key_version_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_definitions_organisation_id_organisations_id_fk": { + "name": "workflow_definitions_organisation_id_organisations_id_fk", + "tableFrom": "workflow_definitions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_definitions_owner_actor_id_actors_id_fk": { + "name": "workflow_definitions_owner_actor_id_actors_id_fk", + "tableFrom": "workflow_definitions", + "tableTo": "actors", + "columnsFrom": [ + "owner_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_runs": { + "name": "workflow_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_definition_id": { + "name": "workflow_definition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "trigger_event_id": { + "name": "trigger_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workflow_runs_org_idempotency_unique": { + "name": "workflow_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_runs_organisation_id_organisations_id_fk": { + "name": "workflow_runs_organisation_id_organisations_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_runs_workflow_definition_id_workflow_definitions_id_fk": { + "name": "workflow_runs_workflow_definition_id_workflow_definitions_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "workflow_definitions", + "columnsFrom": [ + "workflow_definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_runs_room_id_rooms_id_fk": { + "name": "workflow_runs_room_id_rooms_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_runs_investigation_id_investigations_id_fk": { + "name": "workflow_runs_investigation_id_investigations_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_runs_requested_by_actor_id_actors_id_fk": { + "name": "workflow_runs_requested_by_actor_id_actors_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.actor_type": { + "name": "actor_type", + "schema": "public", + "values": [ + "human", + "agent", + "product", + "service", + "system" + ] + }, + "public.alert_status": { + "name": "alert_status", + "schema": "public", + "values": [ + "new", + "acknowledged", + "investigating", + "dismissed", + "promoted", + "closed" + ] + }, + "public.approval_status": { + "name": "approval_status", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected", + "expired", + "cancelled", + "executed", + "failed" + ] + }, + "public.investigation_status": { + "name": "investigation_status", + "schema": "public", + "values": [ + "open", + "triaging", + "investigating", + "awaiting_approval", + "promoted", + "closed" + ] + }, + "public.message_type": { + "name": "message_type", + "schema": "public", + "values": [ + "text", + "system", + "alert", + "finding", + "decision", + "approval", + "workflow", + "agent-status", + "query-result", + "evidence", + "case-event", + "response-action" + ] + }, + "public.room_type": { + "name": "room_type", + "schema": "public", + "values": [ + "operations", + "incident", + "investigation", + "hunt", + "engineering", + "private", + "direct", + "system" + ] + }, + "public.severity": { + "name": "severity", + "schema": "public", + "values": [ + "critical", + "high", + "medium", + "low", + "informational" + ] + }, + "public.task_priority": { + "name": "task_priority", + "schema": "public", + "values": [ + "urgent", + "high", + "normal", + "low" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "backlog", + "ready", + "in_progress", + "review", + "done" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index bd23923..80d036a 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -120,6 +120,13 @@ "when": 1785115079134, "tag": "0016_sticky_ares", "breakpoints": true + }, + { + "idx": 17, + "version": "7", + "when": 1785126003887, + "tag": "0017_daily_dexter_bennett", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/database/src/schema.ts b/packages/database/src/schema.ts index 85b30a1..9d57382 100644 --- a/packages/database/src/schema.ts +++ b/packages/database/src/schema.ts @@ -1638,6 +1638,7 @@ export const tasks = pgTable( agentRunId: text("agent_run_id"), agentRunStatus: text("agent_run_status"), completedAt: timestamp("completed_at", { withTimezone: true }), + archivedAt: timestamp("archived_at", { withTimezone: true }), ...timestamps, }, (table) => [ @@ -1674,6 +1675,7 @@ export const integrationRecords = pgTable( health: jsonb("health").notNull().default({}), cursor: jsonb("cursor").notNull().default({}), lastSyncAt: timestamp("last_sync_at", { withTimezone: true }), + archivedAt: timestamp("archived_at", { withTimezone: true }), ...timestamps, }, (table) => [ @@ -1896,6 +1898,7 @@ export const huntRuns = pgTable( error: text("error"), idempotencyKey: text("idempotency_key").notNull(), completedAt: timestamp("completed_at", { withTimezone: true }), + archivedAt: timestamp("archived_at", { withTimezone: true }), ...timestamps, }, (table) => [ @@ -1979,6 +1982,7 @@ export const researchWatchlists = pgTable( enabled: boolean("enabled").notNull().default(true), nextRunAt: timestamp("next_run_at", { withTimezone: true }).notNull(), lastRunAt: timestamp("last_run_at", { withTimezone: true }), + archivedAt: timestamp("archived_at", { withTimezone: true }), ...timestamps, }, (table) => [ @@ -2056,6 +2060,7 @@ export const reportManifests = pgTable( postedMessageId: uuid("posted_message_id").references(() => messages.id), idempotencyKey: text("idempotency_key").notNull(), reviewedAt: timestamp("reviewed_at", { withTimezone: true }), + archivedAt: timestamp("archived_at", { withTimezone: true }), ...timestamps, }, (table) => [ @@ -2102,6 +2107,7 @@ export const reportSchedules = pgTable( nextRunAt: timestamp("next_run_at", { withTimezone: true }).notNull(), lastRunAt: timestamp("last_run_at", { withTimezone: true }), idempotencyKey: text("idempotency_key").notNull(), + archivedAt: timestamp("archived_at", { withTimezone: true }), ...timestamps, }, (table) => [ From 7ea75b487c8bb05c7371011bde41fa35aa228083 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 14:24:47 +1000 Subject: [PATCH 09/47] fix(seed): preserve declared direct rooms --- packages/database/src/seed-data.test.ts | 31 +++++++++++++++ packages/database/src/seed-data.ts | 43 ++++++++++++++++++++ packages/database/src/seed.ts | 52 +++++++------------------ 3 files changed, 87 insertions(+), 39 deletions(-) create mode 100644 packages/database/src/seed-data.test.ts diff --git a/packages/database/src/seed-data.test.ts b/packages/database/src/seed-data.test.ts new file mode 100644 index 0000000..f26a66f --- /dev/null +++ b/packages/database/src/seed-data.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { demoDirectRoomSeeds, demoIds } from "./seed-data.ts"; + +describe("demonstration direct rooms", () => { + it("seeds every declared direct-room ID with its intended membership", () => { + expect(demoDirectRoomSeeds.map((room) => room.id)).toEqual([ + demoIds.rooms.mayaDirect, + demoIds.rooms.triageDirect, + demoIds.rooms.tawnyDirect, + demoIds.rooms.parkerDirect, + ]); + expect(new Set(demoDirectRoomSeeds.map((room) => room.id)).size).toBe(4); + + expect(demoDirectRoomSeeds.map((room) => room.members).flat()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + actorId: demoIds.actors.triage, + membershipRole: "agent_member", + }), + expect.objectContaining({ + actorId: demoIds.actors.tawnyHunt, + membershipRole: "agent_member", + }), + expect.objectContaining({ + actorId: demoIds.actors.threatIntel, + membershipRole: "agent_member", + }), + ]), + ); + }); +}); diff --git a/packages/database/src/seed-data.ts b/packages/database/src/seed-data.ts index a99aedf..2e30a15 100644 --- a/packages/database/src/seed-data.ts +++ b/packages/database/src/seed-data.ts @@ -51,3 +51,46 @@ export const starterIds = { } as const; export const demoIds = starterIds; + +/** + * Demonstration-only direct rooms. Keep this list aligned with the seed: the + * IDs are consumed by agent allow-lists and room memberships. + */ +export const demoDirectRoomSeeds = [ + { + id: demoIds.rooms.mayaDirect, + slug: "dm-maya-chen", + displayName: "Maya Chen", + members: [ + { actorId: demoIds.actors.jordan, membershipRole: "owner" }, + { actorId: demoIds.actors.maya, membershipRole: "member" }, + ], + }, + { + id: demoIds.rooms.triageDirect, + slug: "dm-triage-agent", + displayName: "Triage Agent", + members: [ + { actorId: demoIds.actors.jordan, membershipRole: "owner" }, + { actorId: demoIds.actors.triage, membershipRole: "agent_member" }, + ], + }, + { + id: demoIds.rooms.tawnyDirect, + slug: "dm-tawny-hunt-agent", + displayName: "Tawny Hunt Agent", + members: [ + { actorId: demoIds.actors.jordan, membershipRole: "owner" }, + { actorId: demoIds.actors.tawnyHunt, membershipRole: "agent_member" }, + ], + }, + { + id: demoIds.rooms.parkerDirect, + slug: "dm-parker", + displayName: "Parker", + members: [ + { actorId: demoIds.actors.jordan, membershipRole: "owner" }, + { actorId: demoIds.actors.threatIntel, membershipRole: "agent_member" }, + ], + }, +] as const; diff --git a/packages/database/src/seed.ts b/packages/database/src/seed.ts index 1e7dfe2..ce1604a 100644 --- a/packages/database/src/seed.ts +++ b/packages/database/src/seed.ts @@ -1,5 +1,5 @@ import { database, closeDatabase, schema } from "./index.ts"; -import { demoIds } from "./seed-data.ts"; +import { demoDirectRoomSeeds, demoIds } from "./seed-data.ts"; import { sql } from "drizzle-orm"; if (process.env.MUSTER_DEMO_MODE !== "true") { @@ -324,7 +324,7 @@ await db "kelpie.cases.read", "audit.read", ], - allowedRooms: [demoIds.rooms.soc], + allowedRooms: [demoIds.rooms.soc, demoIds.rooms.parkerDirect], capabilityRequirements: [ "alerts.read", "investigations.read", @@ -391,16 +391,9 @@ const channelRoomRows = [ "investigation", ], ] as const; -const directRoomRows = [ - [demoIds.rooms.mayaDirect, "dm-maya-chen", "Maya Chen", "direct"], - [demoIds.rooms.triageDirect, "dm-triage-agent", "Triage Agent", "direct"], - [ - demoIds.rooms.tawnyDirect, - "dm-tawny-hunt-agent", - "Tawny Hunt Agent", - "direct", - ], -] as const; +const directRoomRows = demoDirectRoomSeeds.map( + ({ id, slug, displayName }) => [id, slug, displayName, "direct"] as const, +); const roomRows = [...channelRoomRows, ...directRoomRows] as const; await db .insert(schema.rooms) @@ -452,33 +445,14 @@ await db await db .insert(schema.roomMemberships) .values([ - ...[demoIds.actors.jordan, demoIds.actors.maya].map((actorId) => ({ - organisationId: demoIds.organisation, - roomId: demoIds.rooms.mayaDirect, - actorId, - membershipRole: - actorId === demoIds.actors.jordan - ? ("owner" as const) - : ("member" as const), - })), - ...[demoIds.actors.jordan, demoIds.actors.triage].map((actorId) => ({ - organisationId: demoIds.organisation, - roomId: demoIds.rooms.triageDirect, - actorId, - membershipRole: - actorId === demoIds.actors.triage - ? ("agent_member" as const) - : ("owner" as const), - })), - ...[demoIds.actors.jordan, demoIds.actors.tawnyHunt].map((actorId) => ({ - organisationId: demoIds.organisation, - roomId: demoIds.rooms.tawnyDirect, - actorId, - membershipRole: - actorId === demoIds.actors.tawnyHunt - ? ("agent_member" as const) - : ("owner" as const), - })), + ...demoDirectRoomSeeds.flatMap(({ id: roomId, members }) => + members.map(({ actorId, membershipRole }) => ({ + organisationId: demoIds.organisation, + roomId, + actorId, + membershipRole, + })), + ), ]) .onConflictDoNothing(); From 92b3096ac770734a1109bdb0f53e0e166727218f Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 14:32:59 +1000 Subject: [PATCH 10/47] fix(seed): isolate demo identifiers --- apps/web/lib/demo-data.ts | 336 +++++++++++++++++++----- packages/database/src/seed-data.test.ts | 17 +- packages/database/src/seed-data.ts | 65 ++++- packages/database/src/seed.ts | 14 +- 4 files changed, 363 insertions(+), 69 deletions(-) diff --git a/apps/web/lib/demo-data.ts b/apps/web/lib/demo-data.ts index 85203fa..fc8b408 100644 --- a/apps/web/lib/demo-data.ts +++ b/apps/web/lib/demo-data.ts @@ -1,23 +1,85 @@ +const starterIds = { + organisation: "018f55d8-c4c7-7c3e-88ef-000000000001", + actors: { + jordan: "018f55d8-c4c7-7c3e-88ef-000000000010", + triage: "018f55d8-c4c7-7c3e-88ef-000000000020", + tawnyHunt: "018f55d8-c4c7-7c3e-88ef-000000000021", + threatIntel: "018f55d8-c4c7-7c3e-88ef-000000000025", + }, + rooms: { + soc: "018f55d8-c4c7-7c3e-88ef-000000000100", + activeIncidents: "018f55d8-c4c7-7c3e-88ef-000000000101", + threatIntel: "018f55d8-c4c7-7c3e-88ef-000000000102", + detection: "018f55d8-c4c7-7c3e-88ef-000000000103", + endpoint: "018f55d8-c4c7-7c3e-88ef-000000000104", + bower: "018f55d8-c4c7-7c3e-88ef-000000000105", + incident: "018f55d8-c4c7-7c3e-88ef-000000000106", + investigation: "018f55d8-c4c7-7c3e-88ef-000000000107", + alerts: "018f55d8-c4c7-7c3e-88ef-000000000108", + mayaDirect: "018f55d8-c4c7-7c3e-88ef-000000000109", + triageDirect: "018f55d8-c4c7-7c3e-88ef-000000000110", + tawnyDirect: "018f55d8-c4c7-7c3e-88ef-000000000111", + parkerDirect: "018f55d8-c4c7-7c3e-88ef-000000000112", + }, + investigation: "018f55d8-c4c7-7c3e-88ef-000000000200", +} as const; + +const demoIds = { + organisation: "019e7a10-0000-7000-8000-000000000001", + actors: { + jordan: "019e7a10-0000-7000-8000-000000000010", + maya: "019e7a10-0000-7000-8000-000000000011", + daniel: "019e7a10-0000-7000-8000-000000000012", + priya: "019e7a10-0000-7000-8000-000000000013", + alex: "019e7a10-0000-7000-8000-000000000014", + triage: "019e7a10-0000-7000-8000-000000000020", + tawnyHunt: "019e7a10-0000-7000-8000-000000000021", + bowerHealth: "019e7a10-0000-7000-8000-000000000022", + kelpieCase: "019e7a10-0000-7000-8000-000000000023", + sentinelQuery: "019e7a10-0000-7000-8000-000000000024", + threatIntel: "019e7a10-0000-7000-8000-000000000025", + }, + rooms: { + soc: "019e7a10-0000-7000-8000-000000000100", + activeIncidents: "019e7a10-0000-7000-8000-000000000101", + threatIntel: "019e7a10-0000-7000-8000-000000000102", + detection: "019e7a10-0000-7000-8000-000000000103", + endpoint: "019e7a10-0000-7000-8000-000000000104", + bower: "019e7a10-0000-7000-8000-000000000105", + incident: "019e7a10-0000-7000-8000-000000000106", + investigation: "019e7a10-0000-7000-8000-000000000107", + alerts: "019e7a10-0000-7000-8000-000000000108", + mayaDirect: "019e7a10-0000-7000-8000-000000000109", + triageDirect: "019e7a10-0000-7000-8000-000000000110", + tawnyDirect: "019e7a10-0000-7000-8000-000000000111", + parkerDirect: "019e7a10-0000-7000-8000-000000000112", + }, + investigation: "019e7a10-0000-7000-8000-000000000200", + messages: { + mayaParent: "019e7a10-0000-7000-8000-000000000701", + priyaParent: "019e7a10-0000-7000-8000-000000000705", + }, +} as const; + export type Severity = "critical" | "high" | "medium" | "low" | "informational"; -export const demoMode = - process.env.NEXT_PUBLIC_MUSTER_DEMO_MODE === "true"; +export const demoMode = process.env.NEXT_PUBLIC_MUSTER_DEMO_MODE === "true"; export const demoOrganisation = demoMode ? { - id: "018f55d8-c4c7-7c3e-88ef-000000000001", + id: demoIds.organisation, name: "Muster Demo Workspace", slug: "muster-demo", } : { - id: "018f55d8-c4c7-7c3e-88ef-000000000001", + id: starterIds.organisation, name: "Muster Workspace", slug: "muster", }; const demoPeopleRows = [ { - id: "018f55d8-c4c7-7c3e-88ef-000000000010", + id: demoIds.actors.jordan, name: "Jordan Blake", initials: "JB", role: "Security Lead", @@ -25,7 +87,7 @@ const demoPeopleRows = [ type: "human", }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000011", + id: demoIds.actors.maya, name: "Maya Chen", initials: "MC", role: "Senior Analyst", @@ -33,7 +95,7 @@ const demoPeopleRows = [ type: "human", }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000012", + id: demoIds.actors.daniel, name: "Daniel Brooks", initials: "DB", role: "Detection Engineer", @@ -41,7 +103,7 @@ const demoPeopleRows = [ type: "human", }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000013", + id: demoIds.actors.priya, name: "Priya Nair", initials: "PN", role: "Incident Responder", @@ -49,7 +111,7 @@ const demoPeopleRows = [ type: "human", }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000014", + id: demoIds.actors.alex, name: "Alex Morgan", initials: "AM", role: "Read-only Auditor", @@ -62,7 +124,7 @@ export const demoPeople = demoMode ? demoPeopleRows : ([ { - id: "018f55d8-c4c7-7c3e-88ef-000000000010", + id: starterIds.actors.jordan, name: "Muster Administrator", initials: "MA", role: "Administrator", @@ -73,7 +135,7 @@ export const demoPeople = demoMode const starterAgents = [ { - id: "018f55d8-c4c7-7c3e-88ef-000000000020", + id: demoIds.actors.triage, name: "Triage Agent", initials: "TA", purpose: "Correlates alerts and recommends disposition.", @@ -88,7 +150,7 @@ const starterAgents = [ killSwitch: false, }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000021", + id: demoIds.actors.tawnyHunt, name: "Tawny Hunt Agent", initials: "TH", purpose: "Runs bounded endpoint telemetry hunts.", @@ -103,7 +165,7 @@ const starterAgents = [ killSwitch: false, }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000022", + id: demoIds.actors.bowerHealth, name: "Bower Health Agent", initials: "BH", purpose: "Explains collector gaps and delivery posture.", @@ -118,7 +180,7 @@ const starterAgents = [ killSwitch: false, }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000023", + id: demoIds.actors.kelpieCase, name: "Kelpie Case Agent", initials: "KC", purpose: "Drafts and synchronises formal case context.", @@ -133,7 +195,7 @@ const starterAgents = [ killSwitch: false, }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000024", + id: demoIds.actors.sentinelQuery, name: "Sentinel Query Agent", initials: "SQ", purpose: "Builds and runs bounded KQL queries.", @@ -148,7 +210,7 @@ const starterAgents = [ killSwitch: false, }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000025", + id: demoIds.actors.threatIntel, name: "Threat Intelligence Agent", initials: "TI", purpose: "Enriches indicators using approved sources.", @@ -166,9 +228,9 @@ const starterAgents = [ export const demoAgents = demoMode ? starterAgents - : [ + : ([ { - id: "018f55d8-c4c7-7c3e-88ef-000000000020", + id: starterIds.actors.triage, name: "Alfie", initials: "AL", purpose: @@ -184,7 +246,7 @@ export const demoAgents = demoMode killSwitch: false, }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000021", + id: starterIds.actors.tawnyHunt, name: "Jessie", initials: "JE", purpose: @@ -204,7 +266,7 @@ export const demoAgents = demoMode killSwitch: false, }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000025", + id: starterIds.actors.threatIntel, name: "Parker", initials: "PA", purpose: @@ -219,18 +281,90 @@ export const demoAgents = demoMode successRate: "—", killSwitch: false, }, - ] as const; + ] as const); const demoRoomRows = [ - { slug: "soc-operations", name: "soc-operations", topic: "Daily coordination, shift handover and operational updates", unread: 8, mentions: 2, type: "operations", favourite: true }, - { slug: "alerts", name: "alerts", topic: "Incoming security signals and triage discussion", unread: 12, mentions: 3, type: "system", favourite: true }, - { slug: "active-incidents", name: "active-incidents", topic: "Coordination for active security incidents", unread: 4, mentions: 1, type: "incident", favourite: true }, - { slug: "threat-intelligence", name: "threat-intelligence", topic: "Indicator enrichment and intelligence sharing", unread: 0, mentions: 0, type: "operations", favourite: false }, - { slug: "detection-engineering", name: "detection-engineering", topic: "Detection proposals, reviews and releases", unread: 3, mentions: 0, type: "engineering", favourite: false }, - { slug: "endpoint-security", name: "endpoint-security", topic: "Tawny detections, endpoint hunts and response", unread: 0, mentions: 0, type: "operations", favourite: false }, - { slug: "bower-telemetry-health", name: "bower-telemetry-health", topic: "Collector posture, source coverage and delivery health", unread: 1, mentions: 0, type: "system", favourite: false }, - { slug: "incident-KP-2026-0042", name: "incident-KP-2026-0042", topic: "Malicious PowerShell — credential access", unread: 6, mentions: 2, type: "incident", favourite: true }, - { slug: "investigation-suspicious-powershell", name: "investigation-suspicious-powershell", topic: "Correlating Bower identity signals with Tawny endpoint activity", unread: 5, mentions: 1, type: "investigation", favourite: true }, + { + slug: "soc-operations", + name: "soc-operations", + topic: "Daily coordination, shift handover and operational updates", + unread: 8, + mentions: 2, + type: "operations", + favourite: true, + }, + { + slug: "alerts", + name: "alerts", + topic: "Incoming security signals and triage discussion", + unread: 12, + mentions: 3, + type: "system", + favourite: true, + }, + { + slug: "active-incidents", + name: "active-incidents", + topic: "Coordination for active security incidents", + unread: 4, + mentions: 1, + type: "incident", + favourite: true, + }, + { + slug: "threat-intelligence", + name: "threat-intelligence", + topic: "Indicator enrichment and intelligence sharing", + unread: 0, + mentions: 0, + type: "operations", + favourite: false, + }, + { + slug: "detection-engineering", + name: "detection-engineering", + topic: "Detection proposals, reviews and releases", + unread: 3, + mentions: 0, + type: "engineering", + favourite: false, + }, + { + slug: "endpoint-security", + name: "endpoint-security", + topic: "Tawny detections, endpoint hunts and response", + unread: 0, + mentions: 0, + type: "operations", + favourite: false, + }, + { + slug: "bower-telemetry-health", + name: "bower-telemetry-health", + topic: "Collector posture, source coverage and delivery health", + unread: 1, + mentions: 0, + type: "system", + favourite: false, + }, + { + slug: "incident-KP-2026-0042", + name: "incident-KP-2026-0042", + topic: "Malicious PowerShell — credential access", + unread: 6, + mentions: 2, + type: "incident", + favourite: true, + }, + { + slug: "investigation-suspicious-powershell", + name: "investigation-suspicious-powershell", + topic: "Correlating Bower identity signals with Tawny endpoint activity", + unread: 5, + mentions: 1, + type: "investigation", + favourite: true, + }, ] as const; export const demoRooms = demoMode @@ -272,6 +406,14 @@ const demoDirectRoomRows = [ presence: "away", agent: true, }, + { + slug: "dm-parker", + name: "Parker", + topic: "Operational reports and executive briefings", + initials: "PA", + presence: "online", + agent: true, + }, ] as const; export const demoDirectRooms = demoMode @@ -303,22 +445,24 @@ export const demoDirectRooms = demoMode }, ] as const); +const activeIds = demoMode ? demoIds : starterIds; + export const roomIdBySlug: Record = { - "soc-operations": "018f55d8-c4c7-7c3e-88ef-000000000100", - "active-incidents": "018f55d8-c4c7-7c3e-88ef-000000000101", - "threat-intelligence": "018f55d8-c4c7-7c3e-88ef-000000000102", - "detection-engineering": "018f55d8-c4c7-7c3e-88ef-000000000103", - "endpoint-security": "018f55d8-c4c7-7c3e-88ef-000000000104", - "bower-telemetry-health": "018f55d8-c4c7-7c3e-88ef-000000000105", - "incident-KP-2026-0042": "018f55d8-c4c7-7c3e-88ef-000000000106", - "investigation-suspicious-powershell": "018f55d8-c4c7-7c3e-88ef-000000000107", - alerts: "018f55d8-c4c7-7c3e-88ef-000000000108", - "dm-maya-chen": "018f55d8-c4c7-7c3e-88ef-000000000109", - "dm-triage-agent": "018f55d8-c4c7-7c3e-88ef-000000000110", - "dm-tawny-hunt-agent": "018f55d8-c4c7-7c3e-88ef-000000000111", - "dm-alfie": "018f55d8-c4c7-7c3e-88ef-000000000110", - "dm-jessie": "018f55d8-c4c7-7c3e-88ef-000000000111", - "dm-parker": "018f55d8-c4c7-7c3e-88ef-000000000112", + "soc-operations": activeIds.rooms.soc, + "active-incidents": activeIds.rooms.activeIncidents, + "threat-intelligence": activeIds.rooms.threatIntel, + "detection-engineering": activeIds.rooms.detection, + "endpoint-security": activeIds.rooms.endpoint, + "bower-telemetry-health": activeIds.rooms.bower, + "incident-KP-2026-0042": activeIds.rooms.incident, + "investigation-suspicious-powershell": activeIds.rooms.investigation, + alerts: activeIds.rooms.alerts, + "dm-maya-chen": activeIds.rooms.mayaDirect, + "dm-triage-agent": activeIds.rooms.triageDirect, + "dm-tawny-hunt-agent": activeIds.rooms.tawnyDirect, + "dm-alfie": activeIds.rooms.triageDirect, + "dm-jessie": activeIds.rooms.tawnyDirect, + "dm-parker": activeIds.rooms.parkerDirect, }; const demoAlertRows = [ @@ -392,7 +536,7 @@ const demoAlertRows = [ export const demoAlerts = demoMode ? demoAlertRows : []; export const activeInvestigation = { - id: "018f55d8-c4c7-7c3e-88ef-000000000200", + id: activeIds.investigation, number: "INV-2026-0178", title: "Legacy portal credential access and suspicious PowerShell", severity: "critical" as Severity, @@ -410,7 +554,8 @@ export const activeInvestigation = { hypotheses: [ { id: "HYP-12", - statement: "Stolen portal credentials were used before endpoint execution.", + statement: + "Stolen portal credentials were used before endpoint execution.", status: "supported", confidence: 84, support: 4, @@ -499,7 +644,7 @@ const demoRoomTimeline = [ body: "INV-2026-0178 created from ALT-2026-1041 and ALT-2026-1042.", }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000701", + id: demoIds.messages.mayaParent, type: "human", author: "Maya Chen", initials: "MC", @@ -543,7 +688,7 @@ const demoRoomTimeline = [ meta: "FND-87 · 5 evidence references · human reviewed", }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000705", + id: demoIds.messages.priyaParent, type: "human", author: "Priya Nair", initials: "PN", @@ -791,9 +936,23 @@ export const integrationData = { ], rows: [ ["legacy-portal-au-01", "Active", "12 s ago", "0", "Healthy", "3 / 3"], - ["legacy-finance-au-02", "Active", "34 min ago", "284", "Degraded", "4 / 5"], + [ + "legacy-finance-au-02", + "Active", + "34 min ago", + "284", + "Degraded", + "4 / 5", + ], ["customer-api-au-01", "Active", "18 s ago", "0", "Healthy", "6 / 6"], - ["warehouse-erp-au-01", "Pending", "Never", "0", "Awaiting approval", "0 / 4"], + [ + "warehouse-erp-au-01", + "Pending", + "Never", + "0", + "Awaiting approval", + "0 / 4", + ], ], }, tawny: { @@ -810,10 +969,31 @@ export const integrationData = { ["Actions pending", "1"], ], rows: [ - ["WS-1042", "Online", "12 s ago", "Windows 11", "High", "Isolation pending"], + [ + "WS-1042", + "Online", + "12 s ago", + "Windows 11", + "High", + "Isolation pending", + ], ["WS-1098", "Online", "8 s ago", "Windows 11", "Medium", "No action"], - ["SRV-FIN-02", "Online", "21 s ago", "Windows Server 2022", "Low", "No action"], - ["LAP-2041", "Offline", "2 h ago", "macOS 15", "Informational", "No action"], + [ + "SRV-FIN-02", + "Online", + "21 s ago", + "Windows Server 2022", + "Low", + "No action", + ], + [ + "LAP-2041", + "Offline", + "2 h ago", + "macOS 15", + "Informational", + "No action", + ], ], }, kelpie: { @@ -830,10 +1010,38 @@ export const integrationData = { ["Last sync", "42 s"], ], rows: [ - ["KP-2026-0042", "Credential access and endpoint execution", "Containment", "Critical", "Priya Nair", "2 min ago"], - ["KP-2026-0039", "Exposed cloud service principal", "Investigation", "High", "Jordan Blake", "14 min ago"], - ["KP-2026-0037", "Suspicious mailbox forwarding rule", "Monitoring", "Medium", "Maya Chen", "1 h ago"], - ["KP-2026-0033", "Public storage container", "Resolved", "Low", "Daniel Brooks", "Yesterday"], + [ + "KP-2026-0042", + "Credential access and endpoint execution", + "Containment", + "Critical", + "Priya Nair", + "2 min ago", + ], + [ + "KP-2026-0039", + "Exposed cloud service principal", + "Investigation", + "High", + "Jordan Blake", + "14 min ago", + ], + [ + "KP-2026-0037", + "Suspicious mailbox forwarding rule", + "Monitoring", + "Medium", + "Maya Chen", + "1 h ago", + ], + [ + "KP-2026-0033", + "Public storage container", + "Resolved", + "Low", + "Daniel Brooks", + "Yesterday", + ], ], }, } as const; @@ -843,19 +1051,22 @@ const demoSearchResults = [ group: "Messages", title: "Encoded PowerShell retrieved second-stage content", context: "#investigation-suspicious-powershell · Tawny Hunt Agent", - snippet: "Found a PowerShell process tree, two outbound connections, and one file write…", + snippet: + "Found a PowerShell process tree, two outbound connections, and one file write…", }, { group: "Alerts", title: "ALT-2026-1042 · Suspicious PowerShell with encoded command", context: "Tawny · critical · WS-1042", - snippet: "Sigma rule sigma-123 matched powershell.exe with encoded command line.", + snippet: + "Sigma rule sigma-123 matched powershell.exe with encoded command line.", }, { group: "Investigations", title: "INV-2026-0178 · Legacy portal credential access", context: "Awaiting approval · Maya Chen", - snippet: "Bower authentication failures and Tawny endpoint activity correlate on jsmith…", + snippet: + "Bower authentication failures and Tawny endpoint activity correlate on jsmith…", }, { group: "Cases", @@ -867,7 +1078,8 @@ const demoSearchResults = [ group: "Findings", title: "FND-87 · Encoded PowerShell retrieved content", context: "94% confidence · 5 evidence references", - snippet: "Contacted cdn-auth-check.example and wrote update.dat before execution.", + snippet: + "Contacted cdn-auth-check.example and wrote update.dat before execution.", }, { group: "Evidence", diff --git a/packages/database/src/seed-data.test.ts b/packages/database/src/seed-data.test.ts index f26a66f..f1e2cff 100644 --- a/packages/database/src/seed-data.test.ts +++ b/packages/database/src/seed-data.test.ts @@ -1,5 +1,11 @@ import { describe, expect, it } from "vitest"; -import { demoDirectRoomSeeds, demoIds } from "./seed-data.ts"; +import { demoDirectRoomSeeds, demoIds, starterIds } from "./seed-data.ts"; + +function leafIds(value: unknown): string[] { + if (typeof value === "string") return [value]; + if (!value || typeof value !== "object") return []; + return Object.values(value).flatMap(leafIds); +} describe("demonstration direct rooms", () => { it("seeds every declared direct-room ID with its intended membership", () => { @@ -28,4 +34,13 @@ describe("demonstration direct rooms", () => { ]), ); }); + + it("cannot target the clean-install bootstrap organisation", () => { + const bootstrap = new Set(leafIds(starterIds)); + const demo = leafIds(demoIds); + + expect(demoIds.organisation).not.toBe(starterIds.organisation); + expect(new Set(demo).size).toBe(demo.length); + expect(demo.filter((id) => bootstrap.has(id))).toEqual([]); + }); }); diff --git a/packages/database/src/seed-data.ts b/packages/database/src/seed-data.ts index 2e30a15..3ad26b4 100644 --- a/packages/database/src/seed-data.ts +++ b/packages/database/src/seed-data.ts @@ -50,7 +50,70 @@ export const starterIds = { }, } as const; -export const demoIds = starterIds; +/** + * Deterministic IDs for demonstration data only. They must never overlap the + * clean-install bootstrap namespace above: demo seeding is opt-in and must not + * mutate a bootstrap workspace. + */ +export const demoIds = { + organisation: "019e7a10-0000-7000-8000-000000000001", + actors: { + jordan: "019e7a10-0000-7000-8000-000000000010", + maya: "019e7a10-0000-7000-8000-000000000011", + daniel: "019e7a10-0000-7000-8000-000000000012", + priya: "019e7a10-0000-7000-8000-000000000013", + alex: "019e7a10-0000-7000-8000-000000000014", + triage: "019e7a10-0000-7000-8000-000000000020", + tawnyHunt: "019e7a10-0000-7000-8000-000000000021", + bowerHealth: "019e7a10-0000-7000-8000-000000000022", + kelpieCase: "019e7a10-0000-7000-8000-000000000023", + sentinelQuery: "019e7a10-0000-7000-8000-000000000024", + threatIntel: "019e7a10-0000-7000-8000-000000000025", + system: "019e7a10-0000-7000-8000-000000000029", + }, + rooms: { + soc: "019e7a10-0000-7000-8000-000000000100", + activeIncidents: "019e7a10-0000-7000-8000-000000000101", + threatIntel: "019e7a10-0000-7000-8000-000000000102", + detection: "019e7a10-0000-7000-8000-000000000103", + endpoint: "019e7a10-0000-7000-8000-000000000104", + bower: "019e7a10-0000-7000-8000-000000000105", + incident: "019e7a10-0000-7000-8000-000000000106", + investigation: "019e7a10-0000-7000-8000-000000000107", + alerts: "019e7a10-0000-7000-8000-000000000108", + mayaDirect: "019e7a10-0000-7000-8000-000000000109", + triageDirect: "019e7a10-0000-7000-8000-000000000110", + tawnyDirect: "019e7a10-0000-7000-8000-000000000111", + parkerDirect: "019e7a10-0000-7000-8000-000000000112", + }, + investigation: "019e7a10-0000-7000-8000-000000000200", + alerts: { + tawny: "019e7a10-0000-7000-8000-000000000301", + bower: "019e7a10-0000-7000-8000-000000000302", + }, + approval: "019e7a10-0000-7000-8000-000000000401", + findings: { + encodedPowerShell: "019e7a10-0000-7000-8000-000000000501", + }, + integrations: { + kelpie: "019e7a10-0000-7000-8000-000000000601", + tawny: "019e7a10-0000-7000-8000-000000000602", + bower: "019e7a10-0000-7000-8000-000000000603", + }, + tasks: { + threatHunt: "019e7a10-0000-7000-8000-000000000801", + incidentEmail: "019e7a10-0000-7000-8000-000000000802", + executiveUpdate: "019e7a10-0000-7000-8000-000000000803", + monthlyLandscape: "019e7a10-0000-7000-8000-000000000804", + }, + messages: { + mayaParent: "019e7a10-0000-7000-8000-000000000701", + priyaReply: "019e7a10-0000-7000-8000-000000000702", + tawnyReply: "019e7a10-0000-7000-8000-000000000703", + justinReply: "019e7a10-0000-7000-8000-000000000704", + priyaParent: "019e7a10-0000-7000-8000-000000000705", + }, +} as const; /** * Demonstration-only direct rooms. Keep this list aligned with the seed: the diff --git a/packages/database/src/seed.ts b/packages/database/src/seed.ts index ce1604a..94cc238 100644 --- a/packages/database/src/seed.ts +++ b/packages/database/src/seed.ts @@ -266,7 +266,11 @@ await db model: process.env.MUSTER_CODEX_MODEL?.trim() || "configured", ownerActorId: demoIds.actors.jordan, systemPromptVersion: "alfie-v1", - allowedTools: ["alerts.read", "investigations.read", "research.feeds.read"], + allowedTools: [ + "alerts.read", + "investigations.read", + "research.feeds.read", + ], allowedRooms: [demoIds.rooms.soc, demoIds.rooms.triageDirect], capabilityRequirements: [ "alerts.read", @@ -661,7 +665,7 @@ await db await db .insert(schema.findings) .values({ - id: "018f55d8-c4c7-7c3e-88ef-000000000501", + id: demoIds.findings.encodedPowerShell, organisationId: demoIds.organisation, investigationId: demoIds.investigation, createdByActorId: demoIds.actors.tawnyHunt, @@ -705,7 +709,7 @@ await db .insert(schema.integrationRecords) .values([ { - id: "018f55d8-c4c7-7c3e-88ef-000000000601", + id: demoIds.integrations.kelpie, organisationId: demoIds.organisation, product: "kelpie", instanceId: "kelpie-mock-au-01", @@ -714,7 +718,7 @@ await db mock: true, }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000602", + id: demoIds.integrations.tawny, organisationId: demoIds.organisation, product: "tawny", instanceId: "tawny-mock-au-01", @@ -723,7 +727,7 @@ await db mock: true, }, { - id: "018f55d8-c4c7-7c3e-88ef-000000000603", + id: demoIds.integrations.bower, organisationId: demoIds.organisation, product: "bower", instanceId: "bower-mock-au-01", From 619182401699a363f522446bcf41204491f5537b Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 15:12:57 +1000 Subject: [PATCH 11/47] feat(maintenance): govern synthetic cleanup end to end --- .../v1/maintenance/synthetic-cleanup/route.ts | 18 + apps/web/lib/object-storage.ts | 159 +- apps/web/lib/synthetic-cleanup-domain.test.ts | 186 + apps/web/lib/synthetic-cleanup-domain.ts | 121 + apps/worker/package.json | 1 + apps/worker/src/index.ts | 12 + .../src/synthetic-cleanup-object.test.ts | 101 + apps/worker/src/synthetic-cleanup-object.ts | 310 + docs/operations/backup-restore.md | 9 + docs/synthetic-cleanup.md | 98 +- packages/authz/src/index.ts | 8 + .../database/migrations/0018_watery_morg.sql | 37 + .../database/migrations/0019_fuzzy_zemo.sql | 42 + .../migrations/meta/0018_snapshot.json | 11363 +++++++++++++++ .../migrations/meta/0019_snapshot.json | 11655 ++++++++++++++++ .../database/migrations/meta/_journal.json | 14 + packages/database/package.json | 2 +- packages/database/src/schema.ts | 280 +- .../src/synthetic-cleanup.integration.test.ts | 932 ++ .../database/src/synthetic-cleanup.test.ts | 186 +- packages/database/src/synthetic-cleanup.ts | 2048 ++- packages/evidence/src/index.ts | 29 +- packages/evidence/src/object-storage.test.ts | 35 + packages/evidence/src/object-storage.ts | 270 + pnpm-lock.yaml | 6 + 25 files changed, 27603 insertions(+), 319 deletions(-) create mode 100644 apps/web/app/api/v1/maintenance/synthetic-cleanup/route.ts create mode 100644 apps/web/lib/synthetic-cleanup-domain.test.ts create mode 100644 apps/web/lib/synthetic-cleanup-domain.ts create mode 100644 apps/worker/src/synthetic-cleanup-object.test.ts create mode 100644 apps/worker/src/synthetic-cleanup-object.ts create mode 100644 packages/database/migrations/0018_watery_morg.sql create mode 100644 packages/database/migrations/0019_fuzzy_zemo.sql create mode 100644 packages/database/migrations/meta/0018_snapshot.json create mode 100644 packages/database/migrations/meta/0019_snapshot.json create mode 100644 packages/database/src/synthetic-cleanup.integration.test.ts create mode 100644 packages/evidence/src/object-storage.test.ts create mode 100644 packages/evidence/src/object-storage.ts diff --git a/apps/web/app/api/v1/maintenance/synthetic-cleanup/route.ts b/apps/web/app/api/v1/maintenance/synthetic-cleanup/route.ts new file mode 100644 index 0000000..9dbd574 --- /dev/null +++ b/apps/web/app/api/v1/maintenance/synthetic-cleanup/route.ts @@ -0,0 +1,18 @@ +import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; +import { SyntheticCleanupDomainService } from "@/lib/synthetic-cleanup-domain"; + +export async function POST(request: Request) { + const traceId = requestTraceId(request); + try { + const subject = await apiSubject(request); + const body = await request.json(); + const data = await new SyntheticCleanupDomainService().execute( + subject, + body, + traceId, + ); + return Response.json({ data, traceId }); + } catch (error) { + return problemResponse(error, traceId); + } +} diff --git a/apps/web/lib/object-storage.ts b/apps/web/lib/object-storage.ts index 2495071..f33f138 100644 --- a/apps/web/lib/object-storage.ts +++ b/apps/web/lib/object-storage.ts @@ -1,151 +1,8 @@ -import { createHash, createHmac } from "node:crypto"; - -export type EvidenceObject = { - storageKey: string; - contentType: string; - body: Uint8Array; -}; - -export interface EvidenceObjectStorage { - putObject(object: EvidenceObject): Promise; -} - -export interface ContentObjectStorage extends EvidenceObjectStorage { - getObject(storageKey: string): Promise; -} - -function sha256(value: string | Uint8Array) { - return createHash("sha256").update(value).digest("hex"); -} - -function hmac(key: string | Buffer, value: string) { - return createHmac("sha256", key).update(value).digest(); -} - -function objectUrl(endpoint: string, bucket: string, storageKey: string) { - const url = new URL(endpoint); - const basePath = url.pathname.replace(/\/$/, ""); - const encodedKey = storageKey - .split("/") - .map((part) => encodeURIComponent(part)) - .join("/"); - url.pathname = `${basePath}/${encodeURIComponent(bucket)}/${encodedKey}`; - return url; -} - -function signingHeaders( - method: "GET" | "PUT", - url: URL, - body: Uint8Array, - region: string, - accessKey: string, - secretKey: string, - contentType?: string, -) { - const now = new Date(); - const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, ""); - const date = amzDate.slice(0, 8); - const payloadHash = sha256(body); - const canonicalHeaders = [ - ...(contentType ? [`content-type:${contentType}`] : []), - `host:${url.host}`, - `x-amz-content-sha256:${payloadHash}`, - `x-amz-date:${amzDate}`, - ].join("\n"); - const signedHeaders = contentType - ? "content-type;host;x-amz-content-sha256;x-amz-date" - : "host;x-amz-content-sha256;x-amz-date"; - const canonicalRequest = [ - method, - url.pathname, - "", - canonicalHeaders, - "", - signedHeaders, - payloadHash, - ].join("\n"); - const scope = `${date}/${region}/s3/aws4_request`; - const stringToSign = [ - "AWS4-HMAC-SHA256", - amzDate, - scope, - sha256(canonicalRequest), - ].join("\n"); - const dateKey = hmac(`AWS4${secretKey}`, date); - const regionKey = hmac(dateKey, region); - const serviceKey = hmac(regionKey, "s3"); - const signingKey = hmac(serviceKey, "aws4_request"); - const signature = createHmac("sha256", signingKey) - .update(stringToSign) - .digest("hex"); - return { - ...(contentType ? { "content-type": contentType } : {}), - "x-amz-content-sha256": payloadHash, - "x-amz-date": amzDate, - authorization: - `AWS4-HMAC-SHA256 Credential=${accessKey}/${scope},` + - ` SignedHeaders=${signedHeaders}, Signature=${signature}`, - }; -} - -function storageConfiguration() { - return { - endpoint: process.env.OBJECT_STORAGE_ENDPOINT ?? "http://127.0.0.1:9000", - bucket: process.env.OBJECT_STORAGE_BUCKET ?? "muster-evidence", - region: process.env.OBJECT_STORAGE_REGION ?? "us-east-1", - accessKey: process.env.OBJECT_STORAGE_ACCESS_KEY ?? "muster", - secretKey: process.env.OBJECT_STORAGE_SECRET_KEY ?? "local-minio-secret", - }; -} - -export const defaultObjectStorage: ContentObjectStorage = { - async putObject(object) { - const { endpoint, bucket, region, accessKey, secretKey } = - storageConfiguration(); - const url = objectUrl(endpoint, bucket, object.storageKey); - const response = await fetch(url, { - method: "PUT", - headers: signingHeaders( - "PUT", - url, - object.body, - region, - accessKey, - secretKey, - object.contentType, - ), - body: Buffer.from(object.body), - }); - if (!response.ok) { - throw new Error( - `Object storage rejected upload with status ${response.status}`, - ); - } - }, - async getObject(storageKey) { - const { endpoint, bucket, region, accessKey, secretKey } = - storageConfiguration(); - const url = objectUrl(endpoint, bucket, storageKey); - const emptyBody = new Uint8Array(); - const response = await fetch(url, { - method: "GET", - headers: signingHeaders( - "GET", - url, - emptyBody, - region, - accessKey, - secretKey, - ), - }); - if (!response.ok) { - throw new Error( - `Object storage rejected download with status ${response.status}`, - ); - } - return new Uint8Array(await response.arrayBuffer()); - }, -}; - -export const defaultEvidenceObjectStorage: EvidenceObjectStorage = - defaultObjectStorage; +export { + defaultEvidenceObjectStorage, + defaultObjectStorage, + type CleanupObjectStorage, + type ContentObjectStorage, + type EvidenceObject, + type EvidenceObjectStorage, +} from "@muster/evidence"; diff --git a/apps/web/lib/synthetic-cleanup-domain.test.ts b/apps/web/lib/synthetic-cleanup-domain.test.ts new file mode 100644 index 0000000..95ea589 --- /dev/null +++ b/apps/web/lib/synthetic-cleanup-domain.test.ts @@ -0,0 +1,186 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import * as database from "@muster/database"; +import { + syntheticCleanupManifestDigest, + syntheticCleanupTableDigest, + syntheticCleanupTableKeys, + type SyntheticCleanupManifest, + type SyntheticCleanupPlan, +} from "@muster/database"; +import { SyntheticCleanupDomainService } from "./synthetic-cleanup-domain.ts"; + +const evidenceId = "019fa400-0000-7000-8000-000000000001"; +const plan: SyntheticCleanupPlan = { + version: 2, + manifestId: "019fa400-0000-7000-8000-000000000002", + approvalId: "019fa400-0000-7000-8000-000000000003", + organisationId: "019fa400-0000-7000-8000-000000000004", + maintenanceActorId: "019fa400-0000-7000-8000-000000000005", + generatedAt: "2026-07-27T00:00:00.000Z", + archiveRoomIds: [], + archiveTaskIds: [], + archiveHuntIds: [], + archiveIntegrationIds: [], + archiveResearchWatchlistIds: [], + archiveReportManifestIds: [], + archiveReportScheduleIds: [], + hideMessageIds: [], + retireEvidenceIds: [evidenceId], + rejectAgentMemoryIds: [], + retireActorIds: [], + selectionEvidence: [ + { + table: "evidence", + recordId: evidenceId, + provenanceId: "019fa400-0000-7000-8000-000000000006", + }, + ], + objectStorageObjects: [ + { + evidenceId, + bucket: "muster-evidence", + key: `synthetic/${evidenceId}`, + versionId: "synthetic-version-1", + etag: "synthetic-etag", + size: 42, + sha256: "a".repeat(64), + legalHold: false, + objectLockMetadata: {}, + }, + ], +}; + +function manifest(): SyntheticCleanupManifest { + const tableDigests = Object.fromEntries( + syntheticCleanupTableKeys.map((table) => [ + table, + syntheticCleanupTableDigest([]), + ]), + ) as SyntheticCleanupManifest["tableDigests"]; + const unsigned = { ...plan, tableDigests }; + return { + ...unsigned, + digest: syntheticCleanupManifestDigest(unsigned), + }; +} + +const subject = { + actorId: plan.maintenanceActorId, + organisationId: plan.organisationId, + capabilities: new Set(["administration.manage"] as const), +}; + +describe("synthetic cleanup maintenance endpoint", () => { + afterEach(() => vi.restoreAllMocks()); + + it("rejects an unauthorised subject before capture work", async () => { + const capture = vi.spyOn(database, "captureSyntheticCleanupManifest"); + await expect( + new SyntheticCleanupDomainService().execute( + { ...subject, capabilities: new Set() }, + { mode: "capture", payload: plan }, + "trace-forbidden", + ), + ).rejects.toThrow("Missing capability"); + expect(capture).not.toHaveBeenCalled(); + }); + + it("rejects a tampered manifest before database or external work", async () => { + const verify = vi.spyOn(database, "verifySyntheticCleanup"); + await expect( + new SyntheticCleanupDomainService().execute( + subject, + { + mode: "verify", + payload: { ...manifest(), generatedAt: "2026-07-27T01:00:00.000Z" }, + }, + "trace-tampered", + ), + ).rejects.toThrow("digest mismatch"); + expect(verify).not.toHaveBeenCalled(); + }); + + it("queues object deletion instead of doing storage work in HTTP", async () => { + const captured = manifest(); + vi.spyOn(database, "findSyntheticCleanupReceipt").mockResolvedValueOnce( + null, + ); + vi.spyOn(database, "applySyntheticCleanup").mockResolvedValueOnce({ + applied: true, + manifestId: captured.manifestId, + objectStorageObjects: captured.objectStorageObjects, + } as never); + await expect( + new SyntheticCleanupDomainService().execute( + subject, + { mode: "apply", payload: captured }, + "trace-apply", + ), + ).resolves.toMatchObject({ + applied: true, + objectDeletionQueued: true, + pendingObjectVersions: 1, + }); + }); + + it("reports receipt outcomes without requeueing deletion", async () => { + const captured = manifest(); + vi.spyOn(database, "findSyntheticCleanupReceipt").mockResolvedValueOnce({ + manifestId: captured.manifestId, + } as never); + vi.spyOn(database, "applySyntheticCleanup").mockResolvedValueOnce({ + applied: false, + manifestId: captured.manifestId, + receipt: { manifestId: captured.manifestId }, + } as never); + vi.spyOn( + database, + "listSyntheticCleanupObjectDeletionAttempts", + ).mockResolvedValueOnce([ + { + evidenceId, + versionId: "synthetic-version-1", + result: "succeeded", + }, + ] as never); + await expect( + new SyntheticCleanupDomainService().execute( + subject, + { mode: "apply", payload: captured }, + "trace-replay", + ), + ).resolves.toMatchObject({ + applied: false, + objectDeletionQueued: false, + deletedOrReconciledObjectVersions: 1, + pendingObjectVersions: 0, + }); + }); + + it("queues only a freshly authorised retry", async () => { + const captured = manifest(); + vi.spyOn( + database, + "authoriseSyntheticCleanupObjectRetry", + ).mockResolvedValueOnce({ + authorised: true, + pendingObjects: captured.objectStorageObjects, + }); + await expect( + new SyntheticCleanupDomainService().execute( + subject, + { + mode: "retry_object_deletion", + payload: { + manifest: captured, + retryApprovalId: "019fa400-0000-7000-8000-000000000099", + }, + }, + "trace-retry", + ), + ).resolves.toMatchObject({ + authorised: true, + objectDeletionQueued: true, + }); + }); +}); diff --git a/apps/web/lib/synthetic-cleanup-domain.ts b/apps/web/lib/synthetic-cleanup-domain.ts new file mode 100644 index 0000000..9f5eba2 --- /dev/null +++ b/apps/web/lib/synthetic-cleanup-domain.ts @@ -0,0 +1,121 @@ +import { + applySyntheticCleanup, + authoriseSyntheticCleanupObjectRetry, + captureSyntheticCleanupManifest, + findSyntheticCleanupReceipt, + listSyntheticCleanupObjectDeletionAttempts, + parseSyntheticCleanupManifest, + requestSyntheticCleanupApproval, + requestSyntheticCleanupObjectRetryApproval, + SyntheticCleanupObjectRetrySchema, + SyntheticCleanupPlanSchema, + verifySyntheticCleanup, +} from "@muster/database"; +import { + ForbiddenError, + requireCapability, + type AuthorisationSubject, +} from "@muster/authz"; +import { z } from "zod"; + +const RequestSchema = z + .object({ + mode: z.enum([ + "capture", + "verify", + "request_approval", + "apply", + "request_object_deletion_retry", + "retry_object_deletion", + ]), + payload: z.unknown(), + }) + .strict(); + +function validateSubject( + subject: AuthorisationSubject, + payload: { organisationId: string; maintenanceActorId: string }, +) { + requireCapability(subject, "administration.manage"); + if ( + subject.organisationId !== payload.organisationId || + subject.actorId !== payload.maintenanceActorId + ) { + throw new ForbiddenError("administration.manage"); + } +} + +export class SyntheticCleanupDomainService { + async execute(subject: AuthorisationSubject, raw: unknown, traceId: string) { + const input = RequestSchema.parse(raw); + if ( + input.mode === "request_object_deletion_retry" || + input.mode === "retry_object_deletion" + ) { + const retry = SyntheticCleanupObjectRetrySchema.parse(input.payload); + const manifest = parseSyntheticCleanupManifest(retry.manifest); + validateSubject(subject, manifest); + if (input.mode === "request_object_deletion_retry") { + return requestSyntheticCleanupObjectRetryApproval( + subject, + { ...retry, manifest }, + traceId, + ); + } + const authorised = await authoriseSyntheticCleanupObjectRetry( + subject, + { ...retry, manifest }, + traceId, + ); + return { + ...authorised, + objectDeletionQueued: authorised.pendingObjects.length > 0, + }; + } + + if (input.mode === "capture") { + const plan = SyntheticCleanupPlanSchema.parse(input.payload); + validateSubject(subject, plan); + return captureSyntheticCleanupManifest(subject, plan); + } + + const manifest = parseSyntheticCleanupManifest(input.payload); + validateSubject(subject, manifest); + if (input.mode === "verify") { + return verifySyntheticCleanup(subject, manifest); + } + if (input.mode === "request_approval") { + return requestSyntheticCleanupApproval(subject, manifest, traceId); + } + + const priorReceipt = await findSyntheticCleanupReceipt(subject, manifest); + const result = await applySyntheticCleanup(subject, manifest, traceId); + if (!result.applied || priorReceipt) { + const attempts = await listSyntheticCleanupObjectDeletionAttempts( + subject, + manifest, + ); + const completed = new Set( + attempts + .filter( + (attempt) => + attempt.result === "succeeded" || + attempt.result === "observed_missing", + ) + .map((attempt) => `${attempt.evidenceId}:${attempt.versionId}`), + ); + return { + ...result, + deletedOrReconciledObjectVersions: completed.size, + pendingObjectVersions: + manifest.objectStorageObjects.length - completed.size, + objectDeletionQueued: false, + }; + } + return { + ...result, + pendingObjectVersions: manifest.objectStorageObjects.length, + objectDeletionQueued: manifest.objectStorageObjects.length > 0, + }; + } +} diff --git a/apps/worker/package.json b/apps/worker/package.json index 0b16762..38b40aa 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -16,6 +16,7 @@ "@muster/config": "workspace:*", "@muster/contracts": "workspace:*", "@muster/database": "workspace:*", + "@muster/evidence": "workspace:*", "@muster/integrations": "workspace:*", "bullmq": "5.81.2", "drizzle-orm": "0.45.2", diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 7ffddf6..1656010 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -20,6 +20,7 @@ import { schema, writeOutbox, } from "@muster/database"; +import { processSyntheticCleanupObjectDeletion } from "./synthetic-cleanup-object.ts"; import { ConnectorConfigurationSchema, GovernedConnectorError, @@ -111,6 +112,17 @@ const authoritativeProcessor: Processor = async (job) => { job.data.aggregateId, ); } + if ( + job.queueName === "muster-maintenance" && + job.name === "maintenance.synthetic_cleanup.object_delete.queued" + ) { + await processSyntheticCleanupObjectDeletion({ + organisationId: job.data.organisationId, + aggregateType: job.data.aggregateType, + aggregateId: job.data.aggregateId, + traceId: job.data.traceId, + }); + } if ( job.queueName === "muster-maintenance" && job.name === "research.run.queued" diff --git a/apps/worker/src/synthetic-cleanup-object.test.ts b/apps/worker/src/synthetic-cleanup-object.test.ts new file mode 100644 index 0000000..8c0eb65 --- /dev/null +++ b/apps/worker/src/synthetic-cleanup-object.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "vitest"; +import { + assertCleanupObjectContent, + assertCleanupObjectVersion, + canReconcileMissingObject, +} from "./synthetic-cleanup-object.ts"; + +const object = { + evidenceId: "019fa500-0000-7000-8000-000000000001", + bucket: "muster-evidence", + key: "synthetic/evidence.bin", + versionId: "immutable-version-1", + etag: "synthetic-etag", + size: 42, + sha256: "a".repeat(64), + legalHold: false as const, + objectLockMetadata: {}, +}; + +describe("synthetic cleanup object worker", () => { + it("accepts only the exact unlocked immutable version", () => { + expect(() => + assertCleanupObjectVersion(object, { + versionId: object.versionId, + etag: object.etag, + size: object.size, + legalHold: false, + objectLockMetadata: {}, + }), + ).not.toThrow(); + }); + + it.each([ + null, + { + versionId: "replacement-version", + etag: object.etag, + size: object.size, + legalHold: false, + objectLockMetadata: {}, + }, + { + versionId: object.versionId, + etag: object.etag, + size: object.size, + legalHold: true, + objectLockMetadata: {}, + }, + ])("rejects missing, replaced, or held versions", (actual) => { + expect(() => assertCleanupObjectVersion(object, actual)).toThrow( + "metadata changed or is locked", + ); + }); + + it("reconciles missing only after a started attempt or fresh retry approval", () => { + expect( + canReconcileMissingObject( + "cleanup_manifest", + object, + "019fa500-0000-7000-8000-000000000002", + [], + ), + ).toBe(false); + expect( + canReconcileMissingObject( + "cleanup_manifest", + object, + "019fa500-0000-7000-8000-000000000002", + [ + { + evidenceId: object.evidenceId, + versionId: object.versionId, + authorizationApprovalId: "019fa500-0000-7000-8000-000000000002", + result: "started", + }, + ], + ), + ).toBe(true); + expect( + canReconcileMissingObject( + "cleanup_object_retry_approval", + object, + "019fa500-0000-7000-8000-000000000003", + [], + ), + ).toBe(true); + }); + + it("requires the exact version bytes before deletion", () => { + const body = new TextEncoder().encode("synthetic cleanup bytes"); + const expected = { + size: body.byteLength, + sha256: + "14891e06ce2b55d8366b303bda6a77dfdae4ea662102e96b08f767b61458eae2", + }; + expect(() => assertCleanupObjectContent(expected, body)).not.toThrow(); + expect(() => + assertCleanupObjectContent(expected, new TextEncoder().encode("changed")), + ).toThrow("content digest changed"); + }); +}); diff --git a/apps/worker/src/synthetic-cleanup-object.ts b/apps/worker/src/synthetic-cleanup-object.ts new file mode 100644 index 0000000..bb265b9 --- /dev/null +++ b/apps/worker/src/synthetic-cleanup-object.ts @@ -0,0 +1,310 @@ +import { createHash } from "node:crypto"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod"; +import { + database, + parseSyntheticCleanupManifest, + recordSyntheticCleanupObjectDeletionAttempt, + schema, + type SyntheticCleanupObject, +} from "@muster/database"; +import { + defaultObjectStorage, + type CleanupObjectStorage, +} from "@muster/evidence"; + +export type SyntheticCleanupObjectJob = { + organisationId: string; + aggregateType: string; + aggregateId: string; + traceId: string; +}; + +export function assertCleanupObjectVersion( + expected: SyntheticCleanupObject, + actual: Awaited>, +) { + if ( + !actual || + actual.size !== expected.size || + actual.etag !== expected.etag || + actual.versionId !== expected.versionId || + actual.legalHold !== expected.legalHold || + JSON.stringify(actual.objectLockMetadata) !== + JSON.stringify(expected.objectLockMetadata) || + actual.legalHold || + Object.keys(actual.objectLockMetadata).length > 0 + ) { + throw new Error("Cleanup object version metadata changed or is locked"); + } +} + +export function canReconcileMissingObject( + aggregateType: string, + object: Pick, + authorizationApprovalId: string, + attempts: ReadonlyArray<{ + evidenceId: string; + versionId: string; + authorizationApprovalId: string; + result: string; + }>, +) { + return ( + aggregateType === "cleanup_object_retry_approval" || + attempts.some( + (attempt) => + attempt.evidenceId === object.evidenceId && + attempt.versionId === object.versionId && + attempt.authorizationApprovalId === authorizationApprovalId && + attempt.result === "started", + ) + ); +} + +export function assertCleanupObjectContent( + expected: Pick, + body: Uint8Array, +) { + const bodyDigest = createHash("sha256").update(body).digest("hex"); + if (body.byteLength !== expected.size || bodyDigest !== expected.sha256) { + throw new Error("Cleanup object version content digest changed"); + } +} + +export async function processSyntheticCleanupObjectDeletion( + input: SyntheticCleanupObjectJob, + storage: CleanupObjectStorage = defaultObjectStorage, +) { + const db = database(); + let manifestId = input.aggregateId; + let authorizationApprovalId: string | undefined; + if (input.aggregateType === "cleanup_object_retry_approval") { + const [approval] = await db + .select({ + id: schema.approvals.id, + actionType: schema.approvals.actionType, + status: schema.approvals.status, + target: schema.approvals.target, + }) + .from(schema.approvals) + .where( + and( + eq(schema.approvals.organisationId, input.organisationId), + eq(schema.approvals.id, input.aggregateId), + ), + ) + .limit(1); + const target = z + .object({ manifestId: z.uuid() }) + .safeParse(approval?.target); + if ( + !approval || + approval.status !== "executed" || + approval.actionType !== + "maintenance.synthetic-cleanup.object-delete-retry" || + !target.success + ) { + throw new Error("Executed cleanup object retry approval is unavailable"); + } + manifestId = target.data.manifestId; + authorizationApprovalId = approval.id; + } else if (input.aggregateType !== "cleanup_manifest") { + throw new Error("Unsupported cleanup object job target"); + } + + const [receipt] = await db + .select({ + approvalId: schema.syntheticCleanupReceipts.approvalId, + maintenanceActorId: schema.syntheticCleanupReceipts.maintenanceActorId, + manifest: schema.syntheticCleanupReceipts.manifest, + }) + .from(schema.syntheticCleanupReceipts) + .where( + and( + eq( + schema.syntheticCleanupReceipts.organisationId, + input.organisationId, + ), + eq(schema.syntheticCleanupReceipts.manifestId, manifestId), + ), + ) + .limit(1); + if (!receipt) throw new Error("Synthetic cleanup receipt is unavailable"); + const manifest = parseSyntheticCleanupManifest(receipt.manifest); + authorizationApprovalId ??= receipt.approvalId; + + const [actor] = await db + .select({ + status: schema.actors.status, + actorType: schema.actors.actorType, + capabilities: schema.actors.capabilityAssignments, + }) + .from(schema.actors) + .where( + and( + eq(schema.actors.organisationId, input.organisationId), + eq(schema.actors.id, receipt.maintenanceActorId), + ), + ) + .limit(1); + const capabilities = z.array(z.string()).safeParse(actor?.capabilities); + if ( + !actor || + actor.status !== "active" || + actor.actorType !== "human" || + !capabilities.success || + !capabilities.data.includes("administration.manage") + ) { + throw new Error("Cleanup maintenance actor is no longer authorised"); + } + const subject = { + actorId: receipt.maintenanceActorId, + organisationId: input.organisationId, + capabilities: new Set(["administration.manage"] as const), + }; + const attempts = await db + .select({ + evidenceId: schema.syntheticCleanupObjectDeletionAttempts.evidenceId, + versionId: schema.syntheticCleanupObjectDeletionAttempts.versionId, + authorizationApprovalId: + schema.syntheticCleanupObjectDeletionAttempts.authorizationApprovalId, + result: schema.syntheticCleanupObjectDeletionAttempts.result, + }) + .from(schema.syntheticCleanupObjectDeletionAttempts) + .where( + and( + eq( + schema.syntheticCleanupObjectDeletionAttempts.organisationId, + input.organisationId, + ), + eq( + schema.syntheticCleanupObjectDeletionAttempts.manifestId, + manifest.manifestId, + ), + ), + ); + const completed = new Set( + attempts + .filter( + (attempt) => + attempt.result === "succeeded" || + attempt.result === "observed_missing", + ) + .map((attempt) => `${attempt.evidenceId}:${attempt.versionId}`), + ); + const configuredBucket = + process.env.OBJECT_STORAGE_BUCKET ?? "muster-evidence"; + let completedCount = 0; + for (const object of manifest.objectStorageObjects) { + const objectKey = `${object.evidenceId}:${object.versionId}`; + if (completed.has(objectKey)) continue; + let preflightCode = "BucketMismatch"; + let before: Awaited>; + try { + if (object.bucket !== configuredBucket) { + throw new Error("Cleanup object bucket changed"); + } + preflightCode = "MetadataReadFailed"; + before = await storage.headObject(object.key, object.versionId); + } catch (error) { + await recordSyntheticCleanupObjectDeletionAttempt( + subject, + manifest, + object, + authorizationApprovalId, + "failed", + input.traceId, + preflightCode, + ); + throw error; + } + if (!before) { + if ( + !canReconcileMissingObject( + input.aggregateType, + object, + authorizationApprovalId, + attempts, + ) + ) { + await recordSyntheticCleanupObjectDeletionAttempt( + subject, + manifest, + object, + authorizationApprovalId, + "failed", + input.traceId, + "MissingBeforeDeletion", + ); + throw new Error( + "Cleanup object version was never verified before deletion", + ); + } + await recordSyntheticCleanupObjectDeletionAttempt( + subject, + manifest, + object, + authorizationApprovalId, + "observed_missing", + input.traceId, + ); + completedCount += 1; + continue; + } + try { + preflightCode = "MetadataMismatchOrLocked"; + assertCleanupObjectVersion(object, before); + preflightCode = "ContentReadFailed"; + const body = await storage.getObjectVersion(object.key, object.versionId); + preflightCode = "ContentDigestMismatch"; + assertCleanupObjectContent(object, body); + } catch (error) { + await recordSyntheticCleanupObjectDeletionAttempt( + subject, + manifest, + object, + authorizationApprovalId, + "failed", + input.traceId, + preflightCode, + ); + throw error; + } + await recordSyntheticCleanupObjectDeletionAttempt( + subject, + manifest, + object, + authorizationApprovalId, + "started", + input.traceId, + ); + try { + await storage.deleteObject(object.key, object.versionId); + if (await storage.headObject(object.key, object.versionId)) { + throw new Error("Cleanup object version remains after deletion"); + } + await recordSyntheticCleanupObjectDeletionAttempt( + subject, + manifest, + object, + authorizationApprovalId, + "succeeded", + input.traceId, + ); + completedCount += 1; + } catch (error) { + await recordSyntheticCleanupObjectDeletionAttempt( + subject, + manifest, + object, + authorizationApprovalId, + "failed", + input.traceId, + error instanceof Error ? error.name : "UnknownError", + ); + throw error; + } + } + return { completedObjectVersions: completedCount }; +} diff --git a/docs/operations/backup-restore.md b/docs/operations/backup-restore.md index c6b62c0..9e948b4 100644 --- a/docs/operations/backup-restore.md +++ b/docs/operations/backup-restore.md @@ -1,5 +1,14 @@ # Backup and restore +For governed synthetic cleanup, use the stricter two-restore proof in +`docs/synthetic-cleanup.md`. A successful restore command alone is not cleanup +proof. Record source revision, backup SHA-256, per-organisation counts and +ordered row digests, protected direct-message/room/membership rows, evidence +object inventory, and audit-chain result before mutation. Restore the same +backup twice into separate isolated PostgreSQL instances: one to exercise the +exact approved manifest with workers and object credentials disabled, and one +to re-establish the untouched baseline. + Back up PostgreSQL with point-in-time recovery and the evidence bucket with versioning/object lock. Redis is rebuildable execution infrastructure; retain it only for operational continuity. Back up deployment configuration and secret references separately. Restore order: diff --git a/docs/synthetic-cleanup.md b/docs/synthetic-cleanup.md index f899427..42ea9bc 100644 --- a/docs/synthetic-cleanup.md +++ b/docs/synthetic-cleanup.md @@ -1,9 +1,99 @@ # Synthetic cleanup maintenance -`pnpm --filter @muster/database cleanup:synthetic -- --apply /absolute/manifest.json` is intentionally not a selector or broad deletion tool. Supply an independently reviewed immutable JSON manifest only through the maintenance runner. +This runner archives or retires an exact, independently reviewed set of +synthetic records. It is not a selector and has no broad-delete mode. -The manifest contains one organisation, explicit UUID candidates, table digests, and a SHA-256 digest over every field except `digest`. The runner rejects digest changes and the four protected genuine direct-message IDs. +## Safety contract -It uses one serializable transaction. It archives rooms, writes append-only message deletion revisions before hiding proof messages, transitions non-held evidence to `retired` without removing hashes/provenance, rejects selected agent memories, disables selected watchlists, then emits exactly one cleanup audit and outbox event. Audit/outbox rows, evidence metadata, and message history are never deleted. +- One organisation per manifest. Every query and mutation is organisation + scoped. +- Every candidate must have an exact, pre-existing row in the append-only + `synthetic_artifact_provenance` registry. Human-readable names and + idempotency-key text are never cleanup proof. Record provenance when a seed, + mock run, or test fixture creates the artifact; legacy live-proof provenance + requires its own reviewed database change before cleanup capture. +- Authenticated `capture` locks the exact candidate and provenance rows, + records their live SHA-256 table digests, then returns the complete + digest-bound manifest. +- `verify` fails if any candidate is missing, cross-organisation, changed + after capture, already archived, or otherwise protected. +- The maintenance actor must be an active human with + `administration.manage`. +- The authenticated approval-request action creates an expiring + `maintenance.synthetic-cleanup` approval bound to the exact manifest digest. + Apply requires an independent approver, current approver capability, and the + unchanged live pre-state. +- Capture, verify, approval request, and apply require an authenticated web + session whose organisation, actor, and `administration.manage` capability + match the manifest. There is no plan-derived CLI identity. +- The four explicitly protected genuine messages can never be selected. No + direct room, direct-room message, or direct-room member can be archived, + redacted, or retired by this workflow. +- Legal-held, object-locked, already-retired, and stale evidence fails the + whole transaction. Unversioned and versioning-suspended `null` objects are + rejected because they cannot be deleted without a replacement-object race. + Nothing is silently skipped. +- Rooms and top-level product records are archived. Messages receive an + append-only deletion revision before redaction. Evidence metadata and hashes, + execution events/sources, audit, and outbox history remain intact. +- Exact affected-row counts are asserted. One serializable transaction writes + state changes, executed approval, immutable cleanup receipt, audit, and + outbox. +- Cleanup receipts are protected by a database trigger against update/delete. + Object-deletion attempts are append-only too. Replaying the same manifest + returns the receipt and recorded outcomes without deleting objects again. -Object removal needs separate storage evidence after legal-hold and object-lock review; it is deliberately outside this command. Generate candidates from live state, restore-test exact manifest in an isolated PostgreSQL instance, then run with an operator-approved maintenance actor and trace ID. +## Procedure + +1. Capture a PostgreSQL custom-format backup and SHA-256 digest. Inventory each + selected evidence object by bucket, key, version, ETag, size, SHA-256, legal + hold, and object-lock state. The inventory must cover selected evidence + one-to-one. +2. Restore the backup into isolated PostgreSQL with no worker, outbox consumer, + or object-store credentials. Apply the exact application revision's + migrations. +3. Build an unsigned version-2 plan with explicit UUID arrays, + `selectionEvidence`, and `objectStorageObjects`. Supply fresh `manifestId` + and `approvalId` values. +4. Through an authenticated administrator session, POST + `{"mode":"capture","payload":}` to + `/api/v1/maintenance/synthetic-cleanup`. Save and review the returned full + manifest and digest. Never derive an administrator identity from plan JSON. +5. POST `{"mode":"verify","payload":}` and then + `{"mode":"request_approval","payload":}` to + `/api/v1/maintenance/synthetic-cleanup`. Have a different authorised + administrator decide the request through the normal approvals workflow. + +6. In the isolated restore, apply the approved manifest. Assert exact receipt + counts/digests; protected, cross-organisation, bootstrap, and held rows + remain unchanged; the original audit chain only gains requested/applied + events. +7. Restore the original backup into a second isolated database and prove its + baseline counts/digests match the pre-cleanup inventory. Discard both + isolated environments. +8. Repeat capture and approval against live state only if unchanged. Apply by + posting `{"mode":"apply","payload":}` to the authenticated + maintenance endpoint. + +9. Apply commits an idempotent `muster-maintenance` outbox event. The worker, + never the HTTP handler, reloads the immutable receipt and executed approval, + rechecks each exact object version, deletes only receipt-listed immutable + versions, downloads the exact version and verifies its SHA-256, records + append-only started/succeeded/failed outcomes bound to that approval, and + verifies each version is absent after deletion. A missing version fails the + initial job unless the same approval already recorded `started`; this + distinguishes crash recovery from an object that was never verified. + Receipt replays never enqueue deletion. For a failed/crashed deletion, POST + `{"mode":"request_object_deletion_retry","payload":{"manifest":,"retryApprovalId":""}}`; + after a different active administrator approves that exact pending-version + digest, POST the same payload with mode `retry_object_deletion`. This + transaction consumes the approval and queues a new durable worker job. + Missing versions are then recorded as `observed_missing`; present versions + are rechecked and deleted by exact immutable version ID. Refuse held/locked + objects; never infer keys from prefixes. +10. Capture after-counts and a second backup/digest. Restart PostgreSQL, Redis, + MinIO, web, worker, and gateway. Verify health, real login, the four genuine + messages, and all three real agent replies. + +The manifest and receipt contain metadata, never object contents or +credentials. Use synthetic data in tests and documentation. diff --git a/packages/authz/src/index.ts b/packages/authz/src/index.ts index 0d296d8..8302b77 100644 --- a/packages/authz/src/index.ts +++ b/packages/authz/src/index.ts @@ -259,6 +259,14 @@ export const actionApprovalPolicy = { approvalCount: 1, capability: "workflows.approve", }, + "maintenance.synthetic-cleanup": { + approvalCount: 1, + capability: "administration.manage", + }, + "maintenance.synthetic-cleanup.object-delete-retry": { + approvalCount: 1, + capability: "administration.manage", + }, "evidence.delete": { prohibited: true }, } as const satisfies Record< string, diff --git a/packages/database/migrations/0018_watery_morg.sql b/packages/database/migrations/0018_watery_morg.sql new file mode 100644 index 0000000..83b9b89 --- /dev/null +++ b/packages/database/migrations/0018_watery_morg.sql @@ -0,0 +1,37 @@ +CREATE TABLE "synthetic_cleanup_receipts" ( + "manifest_id" uuid PRIMARY KEY NOT NULL, + "organisation_id" uuid NOT NULL, + "approval_id" uuid NOT NULL, + "maintenance_actor_id" uuid NOT NULL, + "manifest_digest" text NOT NULL, + "manifest" jsonb NOT NULL, + "candidate_counts" jsonb NOT NULL, + "pre_digests" jsonb NOT NULL, + "post_digests" jsonb NOT NULL, + "object_storage_objects" jsonb DEFAULT '[]'::jsonb NOT NULL, + "trace_id" text NOT NULL, + "applied_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "synthetic_cleanup_receipts" ADD CONSTRAINT "synthetic_cleanup_receipts_organisation_id_organisations_id_fk" FOREIGN KEY ("organisation_id") REFERENCES "public"."organisations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "synthetic_cleanup_receipts" ADD CONSTRAINT "synthetic_cleanup_receipts_approval_id_approvals_id_fk" FOREIGN KEY ("approval_id") REFERENCES "public"."approvals"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "synthetic_cleanup_receipts" ADD CONSTRAINT "synthetic_cleanup_receipts_maintenance_actor_id_actors_id_fk" FOREIGN KEY ("maintenance_actor_id") REFERENCES "public"."actors"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "synthetic_cleanup_receipts_approval_unique" ON "synthetic_cleanup_receipts" USING btree ("approval_id");--> statement-breakpoint +CREATE UNIQUE INDEX "synthetic_cleanup_receipts_org_digest_unique" ON "synthetic_cleanup_receipts" USING btree ("organisation_id","manifest_digest");--> statement-breakpoint +CREATE FUNCTION "prevent_append_only_mutation"() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + RAISE EXCEPTION '% is append-only', TG_TABLE_NAME; +END; +$$;--> statement-breakpoint +CREATE TRIGGER "synthetic_cleanup_receipts_append_only" +BEFORE UPDATE OR DELETE ON "synthetic_cleanup_receipts" +FOR EACH ROW EXECUTE FUNCTION "prevent_append_only_mutation"();--> statement-breakpoint +CREATE TRIGGER "audit_events_append_only" +BEFORE UPDATE OR DELETE ON "audit_events" +FOR EACH ROW EXECUTE FUNCTION "prevent_append_only_mutation"();--> statement-breakpoint +CREATE TRIGGER "message_revisions_append_only" +BEFORE UPDATE OR DELETE ON "message_revisions" +FOR EACH ROW EXECUTE FUNCTION "prevent_append_only_mutation"(); diff --git a/packages/database/migrations/0019_fuzzy_zemo.sql b/packages/database/migrations/0019_fuzzy_zemo.sql new file mode 100644 index 0000000..799306e --- /dev/null +++ b/packages/database/migrations/0019_fuzzy_zemo.sql @@ -0,0 +1,42 @@ +CREATE TABLE "synthetic_artifact_provenance" ( + "id" uuid PRIMARY KEY NOT NULL, + "organisation_id" uuid NOT NULL, + "artifact_table" text NOT NULL, + "artifact_id" uuid NOT NULL, + "source_kind" text NOT NULL, + "source_reference" text NOT NULL, + "recorded_by_actor_id" uuid NOT NULL, + "recorded_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "synthetic_artifact_provenance_table_check" CHECK ("synthetic_artifact_provenance"."artifact_table" in ('rooms','tasks','hunts','integrations','researchWatchlists','reportManifests','reportSchedules','messages','evidence','agentMemories','actors')), + CONSTRAINT "synthetic_artifact_provenance_source_check" CHECK ("synthetic_artifact_provenance"."source_kind" in ('seed_fixture','mock_runtime','test_fixture','legacy_live_proof')) +); +--> statement-breakpoint +CREATE TABLE "synthetic_cleanup_object_deletion_attempts" ( + "id" uuid PRIMARY KEY NOT NULL, + "manifest_id" uuid NOT NULL, + "organisation_id" uuid NOT NULL, + "evidence_id" uuid NOT NULL, + "version_id" text NOT NULL, + "authorization_approval_id" uuid NOT NULL, + "result" text NOT NULL, + "error_code" text, + "attempted_by_actor_id" uuid NOT NULL, + "trace_id" text NOT NULL, + "attempted_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "synthetic_cleanup_object_attempts_result_check" CHECK ("synthetic_cleanup_object_deletion_attempts"."result" in ('started','succeeded','failed','observed_missing')) +); +--> statement-breakpoint +ALTER TABLE "synthetic_artifact_provenance" ADD CONSTRAINT "synthetic_artifact_provenance_organisation_id_organisations_id_fk" FOREIGN KEY ("organisation_id") REFERENCES "public"."organisations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "synthetic_artifact_provenance" ADD CONSTRAINT "synthetic_artifact_provenance_recorded_by_actor_id_actors_id_fk" FOREIGN KEY ("recorded_by_actor_id") REFERENCES "public"."actors"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "synthetic_cleanup_object_deletion_attempts" ADD CONSTRAINT "synthetic_cleanup_object_deletion_attempts_manifest_id_synthetic_cleanup_receipts_manifest_id_fk" FOREIGN KEY ("manifest_id") REFERENCES "public"."synthetic_cleanup_receipts"("manifest_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "synthetic_cleanup_object_deletion_attempts" ADD CONSTRAINT "synthetic_cleanup_object_deletion_attempts_organisation_id_organisations_id_fk" FOREIGN KEY ("organisation_id") REFERENCES "public"."organisations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "synthetic_cleanup_object_deletion_attempts" ADD CONSTRAINT "synthetic_cleanup_object_deletion_attempts_authorization_approval_id_approvals_id_fk" FOREIGN KEY ("authorization_approval_id") REFERENCES "public"."approvals"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "synthetic_cleanup_object_deletion_attempts" ADD CONSTRAINT "synthetic_cleanup_object_deletion_attempts_attempted_by_actor_id_actors_id_fk" FOREIGN KEY ("attempted_by_actor_id") REFERENCES "public"."actors"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "synthetic_artifact_provenance_artifact_unique" ON "synthetic_artifact_provenance" USING btree ("organisation_id","artifact_table","artifact_id");--> statement-breakpoint +CREATE INDEX "synthetic_cleanup_object_attempts_manifest_idx" ON "synthetic_cleanup_object_deletion_attempts" USING btree ("organisation_id","manifest_id","attempted_at");--> statement-breakpoint +CREATE TRIGGER "synthetic_artifact_provenance_append_only" +BEFORE UPDATE OR DELETE ON "synthetic_artifact_provenance" +FOR EACH ROW EXECUTE FUNCTION "prevent_append_only_mutation"();--> statement-breakpoint +CREATE TRIGGER "synthetic_cleanup_object_attempts_append_only" +BEFORE UPDATE OR DELETE ON "synthetic_cleanup_object_deletion_attempts" +FOR EACH ROW EXECUTE FUNCTION "prevent_append_only_mutation"(); diff --git a/packages/database/migrations/meta/0018_snapshot.json b/packages/database/migrations/meta/0018_snapshot.json new file mode 100644 index 0000000..5edcedc --- /dev/null +++ b/packages/database/migrations/meta/0018_snapshot.json @@ -0,0 +1,11363 @@ +{ + "id": "71b9ed46-7893-4ce6-ba96-f34a03d21ed0", + "prevId": "3f61ffb8-af0d-45ef-a0be-7f325899949f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.actors": { + "name": "actors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "identity_reference": { + "name": "identity_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capability_assignments": { + "name": "capability_assignments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "actors_org_type_idx": { + "name": "actors_org_type_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "actors_org_identity_unique": { + "name": "actors_org_identity_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "identity_reference", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"actors\".\"identity_reference\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "actors_organisation_id_organisations_id_fk": { + "name": "actors_organisation_id_organisations_id_fk", + "tableFrom": "actors", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_definitions": { + "name": "agent_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_actor_id": { + "name": "owner_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "system_prompt_version": { + "name": "system_prompt_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "allowed_rooms": { + "name": "allowed_rooms", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "capability_requirements": { + "name": "capability_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "maximum_runtime_seconds": { + "name": "maximum_runtime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "maximum_token_budget": { + "name": "maximum_token_budget", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20000 + }, + "maximum_cost_cents": { + "name": "maximum_cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 500 + }, + "data_classification_allowance": { + "name": "data_classification_allowance", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"internal\"]'::jsonb" + }, + "approval_requirements": { + "name": "approval_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "requested_permission_mode": { + "name": "requested_permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read_only'" + }, + "kill_switch": { + "name": "kill_switch", + "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": { + "agent_definitions_org_name_unique": { + "name": "agent_definitions_org_name_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_definitions_organisation_id_organisations_id_fk": { + "name": "agent_definitions_organisation_id_organisations_id_fk", + "tableFrom": "agent_definitions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_definitions_owner_actor_id_actors_id_fk": { + "name": "agent_definitions_owner_actor_id_actors_id_fk", + "tableFrom": "agent_definitions", + "tableTo": "actors", + "columnsFrom": [ + "owner_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_definitions_requested_permission_check": { + "name": "agent_definitions_requested_permission_check", + "value": "\"agent_definitions\".\"requested_permission_mode\" in ('read_only','approval_gated')" + } + }, + "isRLSEnabled": false + }, + "public.agent_memories": { + "name": "agent_memories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence_references": { + "name": "evidence_references", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "supersedes_memory_id": { + "name": "supersedes_memory_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reviewed_by_actor_id": { + "name": "reviewed_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_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": { + "agent_memories_org_agent_idx": { + "name": "agent_memories_org_agent_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_memories_organisation_id_organisations_id_fk": { + "name": "agent_memories_organisation_id_organisations_id_fk", + "tableFrom": "agent_memories", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_memories_agent_id_agent_definitions_id_fk": { + "name": "agent_memories_agent_id_agent_definitions_id_fk", + "tableFrom": "agent_memories", + "tableTo": "agent_definitions", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_memories_source_run_id_agent_runs_id_fk": { + "name": "agent_memories_source_run_id_agent_runs_id_fk", + "tableFrom": "agent_memories", + "tableTo": "agent_runs", + "columnsFrom": [ + "source_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_memories_reviewed_by_actor_id_actors_id_fk": { + "name": "agent_memories_reviewed_by_actor_id_actors_id_fk", + "tableFrom": "agent_memories", + "tableTo": "actors", + "columnsFrom": [ + "reviewed_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_memories_kind_check": { + "name": "agent_memories_kind_check", + "value": "\"agent_memories\".\"kind\" in ('fact','preference','lesson','failure','procedure_hint')" + }, + "agent_memories_status_check": { + "name": "agent_memories_status_check", + "value": "\"agent_memories\".\"status\" in ('active','superseded','expired','rejected')" + }, + "agent_memories_confidence_check": { + "name": "agent_memories_confidence_check", + "value": "\"agent_memories\".\"confidence\" between 0 and 100" + } + }, + "isRLSEnabled": false + }, + "public.agent_readiness_snapshots": { + "name": "agent_readiness_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "process_identity": { + "name": "process_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gateway_state": { + "name": "gateway_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authentication_state": { + "name": "authentication_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observer_state": { + "name": "observer_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle_evidence_state": { + "name": "lifecycle_evidence_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle_state": { + "name": "lifecycle_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capability_state": { + "name": "capability_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_state": { + "name": "tool_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_state": { + "name": "permission_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_runtime": { + "name": "reported_runtime", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_provider": { + "name": "reported_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_model": { + "name": "reported_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_capabilities": { + "name": "input_capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_capabilities": { + "name": "output_capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "available_commands": { + "name": "available_commands", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "tool_sources": { + "name": "tool_sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "tool_risk_classes": { + "name": "tool_risk_classes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "requested_permission_mode": { + "name": "requested_permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_permission_mode": { + "name": "effective_permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limitations": { + "name": "limitations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_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()" + } + }, + "indexes": { + "agent_readiness_org_agent_verified_idx": { + "name": "agent_readiness_org_agent_verified_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verified_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_readiness_org_process_verified_idx": { + "name": "agent_readiness_org_process_verified_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "process_identity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verified_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_readiness_snapshots_organisation_id_organisations_id_fk": { + "name": "agent_readiness_snapshots_organisation_id_organisations_id_fk", + "tableFrom": "agent_readiness_snapshots", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_readiness_snapshots_agent_id_agent_definitions_id_fk": { + "name": "agent_readiness_snapshots_agent_id_agent_definitions_id_fk", + "tableFrom": "agent_readiness_snapshots", + "tableTo": "agent_definitions", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_readiness_evidence_states_check": { + "name": "agent_readiness_evidence_states_check", + "value": "\"agent_readiness_snapshots\".\"gateway_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"authentication_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"observer_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"lifecycle_evidence_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"capability_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"tool_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"permission_state\" in ('reported','unavailable','unknown')" + }, + "agent_readiness_lifecycle_state_check": { + "name": "agent_readiness_lifecycle_state_check", + "value": "\"agent_readiness_snapshots\".\"lifecycle_state\" in ('idle','running','stopped','failed','unknown')" + }, + "agent_readiness_permission_modes_check": { + "name": "agent_readiness_permission_modes_check", + "value": "\"agent_readiness_snapshots\".\"requested_permission_mode\" in ('read_only','approval_gated','unknown')\n and \"agent_readiness_snapshots\".\"effective_permission_mode\" in ('read_only','approval_gated','unknown')" + } + }, + "isRLSEnabled": false + }, + "public.agent_run_events": { + "name": "agent_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_run_events_org_run_idx": { + "name": "agent_run_events_org_run_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_events_organisation_id_organisations_id_fk": { + "name": "agent_run_events_organisation_id_organisations_id_fk", + "tableFrom": "agent_run_events", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_run_events_run_id_agent_runs_id_fk": { + "name": "agent_run_events_run_id_agent_runs_id_fk", + "tableFrom": "agent_run_events", + "tableTo": "agent_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_run_sources": { + "name": "agent_run_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_run_sources_org_run_unique": { + "name": "agent_run_sources_org_run_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_run_sources_org_run_idx": { + "name": "agent_run_sources_org_run_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_sources_organisation_id_organisations_id_fk": { + "name": "agent_run_sources_organisation_id_organisations_id_fk", + "tableFrom": "agent_run_sources", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_run_sources_run_id_agent_runs_id_fk": { + "name": "agent_run_sources_run_id_agent_runs_id_fk", + "tableFrom": "agent_run_sources", + "tableTo": "agent_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runs": { + "name": "agent_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "progress": { + "name": "progress", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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 + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline_at": { + "name": "deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "worker_id": { + "name": "worker_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt_hash": { + "name": "prompt_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_hash": { + "name": "output_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_schema": { + "name": "output_schema", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "maximum_runtime_seconds": { + "name": "maximum_runtime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "maximum_token_budget": { + "name": "maximum_token_budget", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20000 + }, + "maximum_cost_cents": { + "name": "maximum_cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 500 + }, + "token_usage": { + "name": "token_usage", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "estimated_cost_cents": { + "name": "estimated_cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_call_count": { + "name": "tool_call_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "diagnostics": { + "name": "diagnostics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "structured_output": { + "name": "structured_output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_runs_org_idempotency_unique": { + "name": "agent_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runs_org_status_idx": { + "name": "agent_runs_org_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runs_recovery_idx": { + "name": "agent_runs_recovery_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runs_agent_id_agent_definitions_id_fk": { + "name": "agent_runs_agent_id_agent_definitions_id_fk", + "tableFrom": "agent_runs", + "tableTo": "agent_definitions", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runs_organisation_id_organisations_id_fk": { + "name": "agent_runs_organisation_id_organisations_id_fk", + "tableFrom": "agent_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runs_room_id_rooms_id_fk": { + "name": "agent_runs_room_id_rooms_id_fk", + "tableFrom": "agent_runs", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runs_investigation_id_investigations_id_fk": { + "name": "agent_runs_investigation_id_investigations_id_fk", + "tableFrom": "agent_runs", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runs_requested_by_actor_id_actors_id_fk": { + "name": "agent_runs_requested_by_actor_id_actors_id_fk", + "tableFrom": "agent_runs", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_skill_evaluations": { + "name": "agent_skill_evaluations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_version_id": { + "name": "skill_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "evaluator_actor_id": { + "name": "evaluator_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "suite": { + "name": "suite", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "passed": { + "name": "passed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "baseline_score": { + "name": "baseline_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "regressions": { + "name": "regressions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_skill_evaluations_version_idx": { + "name": "agent_skill_evaluations_version_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_skill_evaluations_organisation_id_organisations_id_fk": { + "name": "agent_skill_evaluations_organisation_id_organisations_id_fk", + "tableFrom": "agent_skill_evaluations", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_evaluations_skill_version_id_agent_skill_versions_id_fk": { + "name": "agent_skill_evaluations_skill_version_id_agent_skill_versions_id_fk", + "tableFrom": "agent_skill_evaluations", + "tableTo": "agent_skill_versions", + "columnsFrom": [ + "skill_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_evaluations_evaluator_actor_id_actors_id_fk": { + "name": "agent_skill_evaluations_evaluator_actor_id_actors_id_fk", + "tableFrom": "agent_skill_evaluations", + "tableTo": "actors", + "columnsFrom": [ + "evaluator_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_skill_evaluations_score_check": { + "name": "agent_skill_evaluations_score_check", + "value": "\"agent_skill_evaluations\".\"score\" between 0 and 100" + } + }, + "isRLSEnabled": false + }, + "public.agent_skill_versions": { + "name": "agent_skill_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "based_on_version_id": { + "name": "based_on_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_rationale": { + "name": "change_rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence_references": { + "name": "evidence_references", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "required_capabilities": { + "name": "required_capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'proposed'" + }, + "approved_by_actor_id": { + "name": "approved_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_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": { + "agent_skill_versions_skill_version_unique": { + "name": "agent_skill_versions_skill_version_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_skill_versions_content_hash_unique": { + "name": "agent_skill_versions_content_hash_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "content_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_skill_versions_organisation_id_organisations_id_fk": { + "name": "agent_skill_versions_organisation_id_organisations_id_fk", + "tableFrom": "agent_skill_versions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_versions_skill_id_agent_skills_id_fk": { + "name": "agent_skill_versions_skill_id_agent_skills_id_fk", + "tableFrom": "agent_skill_versions", + "tableTo": "agent_skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_versions_source_run_id_agent_runs_id_fk": { + "name": "agent_skill_versions_source_run_id_agent_runs_id_fk", + "tableFrom": "agent_skill_versions", + "tableTo": "agent_runs", + "columnsFrom": [ + "source_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_versions_approved_by_actor_id_actors_id_fk": { + "name": "agent_skill_versions_approved_by_actor_id_actors_id_fk", + "tableFrom": "agent_skill_versions", + "tableTo": "actors", + "columnsFrom": [ + "approved_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_skill_versions_state_check": { + "name": "agent_skill_versions_state_check", + "value": "\"agent_skill_versions\".\"state\" in ('proposed','evaluating','approved','rejected','published','rolled_back')" + } + }, + "isRLSEnabled": false + }, + "public.agent_skills": { + "name": "agent_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_key": { + "name": "skill_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "active_version_id": { + "name": "active_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "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": { + "agent_skills_org_agent_key_unique": { + "name": "agent_skills_org_agent_key_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_skills_organisation_id_organisations_id_fk": { + "name": "agent_skills_organisation_id_organisations_id_fk", + "tableFrom": "agent_skills", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skills_agent_id_agent_definitions_id_fk": { + "name": "agent_skills_agent_id_agent_definitions_id_fk", + "tableFrom": "agent_skills", + "tableTo": "agent_definitions", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skills_created_by_actor_id_actors_id_fk": { + "name": "agent_skills_created_by_actor_id_actors_id_fk", + "tableFrom": "agent_skills", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_skills_status_check": { + "name": "agent_skills_status_check", + "value": "\"agent_skills\".\"status\" in ('draft','evaluating','published','retired')" + } + }, + "isRLSEnabled": false + }, + "public.agent_tool_calls": { + "name": "agent_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "arguments_hash": { + "name": "arguments_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result_hash": { + "name": "result_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_tool_calls_org_run_idx": { + "name": "agent_tool_calls_org_run_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_tool_calls_organisation_id_organisations_id_fk": { + "name": "agent_tool_calls_organisation_id_organisations_id_fk", + "tableFrom": "agent_tool_calls", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_tool_calls_run_id_agent_runs_id_fk": { + "name": "agent_tool_calls_run_id_agent_runs_id_fk", + "tableFrom": "agent_tool_calls", + "tableTo": "agent_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_tool_calls_approval_id_approvals_id_fk": { + "name": "agent_tool_calls_approval_id_approvals_id_fk", + "tableFrom": "agent_tool_calls", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alerts": { + "name": "alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_product": { + "name": "source_product", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_instance": { + "name": "source_instance", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_reference": { + "name": "external_reference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "severity": { + "name": "severity", + "type": "severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "alert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "rule_name": { + "name": "rule_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "assigned_actor_id": { + "name": "assigned_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "entities": { + "name": "entities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "observables": { + "name": "observables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "raw_reference_metadata": { + "name": "raw_reference_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kelpie_case_id": { + "name": "kelpie_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_key": { + "name": "correlation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "alerts_org_source_ref_unique": { + "name": "alerts_org_source_ref_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_instance", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_reference", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alerts_org_dedupe_unique": { + "name": "alerts_org_dedupe_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alerts_org_queue_idx": { + "name": "alerts_org_queue_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alerts_search_idx": { + "name": "alerts_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"title\" || ' ' || \"description\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "alerts_organisation_id_organisations_id_fk": { + "name": "alerts_organisation_id_organisations_id_fk", + "tableFrom": "alerts", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "alerts_assigned_actor_id_actors_id_fk": { + "name": "alerts_assigned_actor_id_actors_id_fk", + "tableFrom": "alerts", + "tableTo": "actors", + "columnsFrom": [ + "assigned_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "alerts_room_id_rooms_id_fk": { + "name": "alerts_room_id_rooms_id_fk", + "tableFrom": "alerts", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requesting_actor_id": { + "name": "requesting_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "risk_summary": { + "name": "risk_summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "required_capability": { + "name": "required_capability", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "required_approval_count": { + "name": "required_approval_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "decisions": { + "name": "decisions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "decision_at": { + "name": "decision_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "approvals_org_idempotency_unique": { + "name": "approvals_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approvals_org_status_idx": { + "name": "approvals_org_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_organisation_id_organisations_id_fk": { + "name": "approvals_organisation_id_organisations_id_fk", + "tableFrom": "approvals", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requesting_actor_id_actors_id_fk": { + "name": "approvals_requesting_actor_id_actors_id_fk", + "tableFrom": "approvals", + "tableTo": "actors", + "columnsFrom": [ + "requesting_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_events": { + "name": "audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previous_hash": { + "name": "previous_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_hash": { + "name": "event_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_org_sequence_unique": { + "name": "audit_org_sequence_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_org_hash_unique": { + "name": "audit_org_hash_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_org_target_idx": { + "name": "audit_org_target_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_events_organisation_id_organisations_id_fk": { + "name": "audit_events_organisation_id_organisations_id_fk", + "tableFrom": "audit_events", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "audit_events_actor_id_actors_id_fk": { + "name": "audit_events_actor_id_actors_id_fk", + "tableFrom": "audit_events", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_account": { + "name": "auth_account", + "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": { + "auth_accounts_user_idx": { + "name": "auth_accounts_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_accounts_provider_unique": { + "name": "auth_accounts_provider_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_account_user_id_auth_user_id_fk": { + "name": "auth_account_user_id_auth_user_id_fk", + "tableFrom": "auth_account", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_passkey": { + "name": "auth_passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_passkey_user_idx": { + "name": "auth_passkey_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_passkey_user_id_auth_user_id_fk": { + "name": "auth_passkey_user_id_auth_user_id_fk", + "tableFrom": "auth_passkey", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_passkey_credential_id_unique": { + "name": "auth_passkey_credential_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_session": { + "name": "auth_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "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 + }, + "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": { + "auth_sessions_user_idx": { + "name": "auth_sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_session_user_id_auth_user_id_fk": { + "name": "auth_session_user_id_auth_user_id_fk", + "tableFrom": "auth_session", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_session_token_unique": { + "name": "auth_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_two_factor": { + "name": "auth_two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_two_factor_user_unique": { + "name": "auth_two_factor_user_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_two_factor_user_id_auth_user_id_fk": { + "name": "auth_two_factor_user_id_auth_user_id_fk", + "tableFrom": "auth_two_factor", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_user": { + "name": "auth_user", + "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 + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_user_email_unique": { + "name": "auth_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_verification": { + "name": "auth_verification", + "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": { + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decisions": { + "name": "decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decision_maker_actor_id": { + "name": "decision_maker_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "alternatives_considered": { + "name": "alternatives_considered", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "evidence_references": { + "name": "evidence_references", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "related_investigation_id": { + "name": "related_investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_case_id": { + "name": "related_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_workflow_run_id": { + "name": "related_workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decisions_org_investigation_idx": { + "name": "decisions_org_investigation_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_investigation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decisions_organisation_id_organisations_id_fk": { + "name": "decisions_organisation_id_organisations_id_fk", + "tableFrom": "decisions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_decision_maker_actor_id_actors_id_fk": { + "name": "decisions_decision_maker_actor_id_actors_id_fk", + "tableFrom": "decisions", + "tableTo": "actors", + "columnsFrom": [ + "decision_maker_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_related_investigation_id_investigations_id_fk": { + "name": "decisions_related_investigation_id_investigations_id_fk", + "tableFrom": "decisions", + "tableTo": "investigations", + "columnsFrom": [ + "related_investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.evidence": { + "name": "evidence", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by_actor_id": { + "name": "uploaded_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "related_room_id": { + "name": "related_room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_investigation_id": { + "name": "related_investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_case_id": { + "name": "related_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_timestamp": { + "name": "original_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scan_state": { + "name": "scan_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "retention_state": { + "name": "retention_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "legal_hold": { + "name": "legal_hold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "object_lock_metadata": { + "name": "object_lock_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "evidence_org_hash_unique": { + "name": "evidence_org_hash_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "evidence_search_idx": { + "name": "evidence_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"file_name\" || ' ' || \"source\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "evidence_organisation_id_organisations_id_fk": { + "name": "evidence_organisation_id_organisations_id_fk", + "tableFrom": "evidence", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evidence_uploaded_by_actor_id_actors_id_fk": { + "name": "evidence_uploaded_by_actor_id_actors_id_fk", + "tableFrom": "evidence", + "tableTo": "actors", + "columnsFrom": [ + "uploaded_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evidence_related_room_id_rooms_id_fk": { + "name": "evidence_related_room_id_rooms_id_fk", + "tableFrom": "evidence", + "tableTo": "rooms", + "columnsFrom": [ + "related_room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evidence_related_investigation_id_investigations_id_fk": { + "name": "evidence_related_investigation_id_investigations_id_fk", + "tableFrom": "evidence", + "tableTo": "investigations", + "columnsFrom": [ + "related_investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.findings": { + "name": "findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "supporting_evidence": { + "name": "supporting_evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "related_entities": { + "name": "related_entities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "related_observables": { + "name": "related_observables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recommended_action": { + "name": "recommended_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_provenance": { + "name": "agent_provenance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "human_reviewed_at": { + "name": "human_reviewed_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()" + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "findings_org_investigation_idx": { + "name": "findings_org_investigation_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "investigation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "findings_search_idx": { + "name": "findings_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"title\" || ' ' || \"summary\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "findings_organisation_id_organisations_id_fk": { + "name": "findings_organisation_id_organisations_id_fk", + "tableFrom": "findings", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "findings_investigation_id_investigations_id_fk": { + "name": "findings_investigation_id_investigations_id_fk", + "tableFrom": "findings", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "findings_created_by_actor_id_actors_id_fk": { + "name": "findings_created_by_actor_id_actors_id_fk", + "tableFrom": "findings", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hunt_queries": { + "name": "hunt_queries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "hunt_id": { + "name": "hunt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "query_run_id": { + "name": "query_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "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": { + "hunt_queries_org_query_run_unique": { + "name": "hunt_queries_org_query_run_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "query_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hunt_queries_org_hunt_sequence_unique": { + "name": "hunt_queries_org_hunt_sequence_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hunt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hunt_queries_org_hunt_idx": { + "name": "hunt_queries_org_hunt_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hunt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hunt_queries_organisation_id_organisations_id_fk": { + "name": "hunt_queries_organisation_id_organisations_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_queries_hunt_id_hunt_runs_id_fk": { + "name": "hunt_queries_hunt_id_hunt_runs_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "hunt_runs", + "columnsFrom": [ + "hunt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_queries_integration_id_integration_records_id_fk": { + "name": "hunt_queries_integration_id_integration_records_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_queries_template_id_integration_query_templates_id_fk": { + "name": "hunt_queries_template_id_integration_query_templates_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "integration_query_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_queries_query_run_id_integration_query_runs_id_fk": { + "name": "hunt_queries_query_run_id_integration_query_runs_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "integration_query_runs", + "columnsFrom": [ + "query_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hunt_runs": { + "name": "hunt_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_message_id": { + "name": "source_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_case_id": { + "name": "linked_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "training_mode": { + "name": "training_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "plan": { + "name": "plan", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hunt_runs_org_idempotency_unique": { + "name": "hunt_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hunt_runs_org_agent_run_unique": { + "name": "hunt_runs_org_agent_run_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hunt_runs_org_status_idx": { + "name": "hunt_runs_org_status_idx", + "columns": [ + { + "expression": "organisation_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": { + "hunt_runs_organisation_id_organisations_id_fk": { + "name": "hunt_runs_organisation_id_organisations_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_agent_run_id_agent_runs_id_fk": { + "name": "hunt_runs_agent_run_id_agent_runs_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "agent_runs", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_task_id_tasks_id_fk": { + "name": "hunt_runs_task_id_tasks_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_source_message_id_messages_id_fk": { + "name": "hunt_runs_source_message_id_messages_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "messages", + "columnsFrom": [ + "source_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_room_id_rooms_id_fk": { + "name": "hunt_runs_room_id_rooms_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_requested_by_actor_id_actors_id_fk": { + "name": "hunt_runs_requested_by_actor_id_actors_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_approval_id_approvals_id_fk": { + "name": "hunt_runs_approval_id_approvals_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "hunt_runs_status_check": { + "name": "hunt_runs_status_check", + "value": "\"hunt_runs\".\"status\" in ('planned','awaiting_approval','querying','analysing','completed','failed','cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.hypotheses": { + "name": "hypotheses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "statement": { + "name": "statement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unverified'" + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "supporting_finding_ids": { + "name": "supporting_finding_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "contradicting_finding_ids": { + "name": "contradicting_finding_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "hypotheses_org_investigation_idx": { + "name": "hypotheses_org_investigation_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "investigation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hypotheses_organisation_id_organisations_id_fk": { + "name": "hypotheses_organisation_id_organisations_id_fk", + "tableFrom": "hypotheses", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hypotheses_investigation_id_investigations_id_fk": { + "name": "hypotheses_investigation_id_investigations_id_fk", + "tableFrom": "hypotheses", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hypotheses_created_by_actor_id_actors_id_fk": { + "name": "hypotheses_created_by_actor_id_actors_id_fk", + "tableFrom": "hypotheses", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_records": { + "name": "idempotency_records", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idempotency_expiry_idx": { + "name": "idempotency_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "idempotency_records_organisation_id_organisations_id_fk": { + "name": "idempotency_records_organisation_id_organisations_id_fk", + "tableFrom": "idempotency_records", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "idempotency_records_organisation_id_scope_key_pk": { + "name": "idempotency_records_organisation_id_scope_key_pk", + "columns": [ + "organisation_id", + "scope", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connector_credentials": { + "name": "integration_connector_credentials", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "encrypted_credential": { + "name": "encrypted_credential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "envelope_version": { + "name": "envelope_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'v1'" + }, + "rotation_version": { + "name": "rotation_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "rotated_by_actor_id": { + "name": "rotated_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_credentials_org_idx": { + "name": "integration_credentials_org_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_connector_credentials_organisation_id_organisations_id_fk": { + "name": "integration_connector_credentials_organisation_id_organisations_id_fk", + "tableFrom": "integration_connector_credentials", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_connector_credentials_integration_id_integration_records_id_fk": { + "name": "integration_connector_credentials_integration_id_integration_records_id_fk", + "tableFrom": "integration_connector_credentials", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_connector_credentials_rotated_by_actor_id_actors_id_fk": { + "name": "integration_connector_credentials_rotated_by_actor_id_actors_id_fk", + "tableFrom": "integration_connector_credentials", + "tableTo": "actors", + "columnsFrom": [ + "rotated_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_deliveries": { + "name": "integration_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_metadata": { + "name": "request_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "response_metadata": { + "name": "response_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_delivery_org_idempotency_unique": { + "name": "integration_delivery_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_deliveries_organisation_id_organisations_id_fk": { + "name": "integration_deliveries_organisation_id_organisations_id_fk", + "tableFrom": "integration_deliveries", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_deliveries_integration_id_integration_records_id_fk": { + "name": "integration_deliveries_integration_id_integration_records_id_fk", + "tableFrom": "integration_deliveries", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_entities": { + "name": "integration_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "posture": { + "name": "posture", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_entities_org_external_unique": { + "name": "integration_entities_org_external_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_entities_organisation_id_organisations_id_fk": { + "name": "integration_entities_organisation_id_organisations_id_fk", + "tableFrom": "integration_entities", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_entities_integration_id_integration_records_id_fk": { + "name": "integration_entities_integration_id_integration_records_id_fk", + "tableFrom": "integration_entities", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_query_runs": { + "name": "integration_query_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "request_metadata": { + "name": "request_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "response_metadata": { + "name": "response_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_query_runs_org_idempotency_unique": { + "name": "integration_query_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_query_runs_org_status_idx": { + "name": "integration_query_runs_org_status_idx", + "columns": [ + { + "expression": "organisation_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": { + "integration_query_runs_organisation_id_organisations_id_fk": { + "name": "integration_query_runs_organisation_id_organisations_id_fk", + "tableFrom": "integration_query_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_runs_integration_id_integration_records_id_fk": { + "name": "integration_query_runs_integration_id_integration_records_id_fk", + "tableFrom": "integration_query_runs", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_runs_template_id_integration_query_templates_id_fk": { + "name": "integration_query_runs_template_id_integration_query_templates_id_fk", + "tableFrom": "integration_query_runs", + "tableTo": "integration_query_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_runs_requested_by_actor_id_actors_id_fk": { + "name": "integration_query_runs_requested_by_actor_id_actors_id_fk", + "tableFrom": "integration_query_runs", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_query_templates": { + "name": "integration_query_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_key": { + "name": "template_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "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": { + "integration_templates_org_key_version_unique": { + "name": "integration_templates_org_key_version_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_query_templates_organisation_id_organisations_id_fk": { + "name": "integration_query_templates_organisation_id_organisations_id_fk", + "tableFrom": "integration_query_templates", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_templates_integration_id_integration_records_id_fk": { + "name": "integration_query_templates_integration_id_integration_records_id_fk", + "tableFrom": "integration_query_templates", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_templates_created_by_actor_id_actors_id_fk": { + "name": "integration_query_templates_created_by_actor_id_actors_id_fk", + "tableFrom": "integration_query_templates", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_records": { + "name": "integration_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mock": { + "name": "mock", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "configuration": { + "name": "configuration", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "health": { + "name": "health", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cursor": { + "name": "cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integrations_org_product_instance_unique": { + "name": "integrations_org_product_instance_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_records_organisation_id_organisations_id_fk": { + "name": "integration_records_organisation_id_organisations_id_fk", + "tableFrom": "integration_records", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.investigations": { + "name": "investigations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "investigation_number": { + "name": "investigation_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "investigation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "severity": { + "name": "severity", + "type": "severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "lead_actor_id": { + "name": "lead_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recommendation": { + "name": "recommendation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promotion_decision": { + "name": "promotion_decision", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "linked_kelpie_case_id": { + "name": "linked_kelpie_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "investigations_org_number_unique": { + "name": "investigations_org_number_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "investigation_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "investigations_org_queue_idx": { + "name": "investigations_org_queue_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_activity_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "investigations_search_idx": { + "name": "investigations_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"title\" || ' ' || \"summary\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "investigations_organisation_id_organisations_id_fk": { + "name": "investigations_organisation_id_organisations_id_fk", + "tableFrom": "investigations", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "investigations_lead_actor_id_actors_id_fk": { + "name": "investigations_lead_actor_id_actors_id_fk", + "tableFrom": "investigations", + "tableTo": "actors", + "columnsFrom": [ + "lead_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "investigations_room_id_rooms_id_fk": { + "name": "investigations_room_id_rooms_id_fk", + "tableFrom": "investigations", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_mentions": { + "name": "message_mentions", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mentioned_actor_id": { + "name": "mentioned_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mention_type": { + "name": "mention_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mention_key": { + "name": "mention_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_mentions_org_actor_idx": { + "name": "message_mentions_org_actor_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mentioned_actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_mentions_organisation_id_organisations_id_fk": { + "name": "message_mentions_organisation_id_organisations_id_fk", + "tableFrom": "message_mentions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_mentions_message_id_messages_id_fk": { + "name": "message_mentions_message_id_messages_id_fk", + "tableFrom": "message_mentions", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_mentions_mentioned_actor_id_actors_id_fk": { + "name": "message_mentions_mentioned_actor_id_actors_id_fk", + "tableFrom": "message_mentions", + "tableTo": "actors", + "columnsFrom": [ + "mentioned_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_mentions_message_id_mention_type_mention_key_pk": { + "name": "message_mentions_message_id_mention_type_mention_key_pk", + "columns": [ + "message_id", + "mention_type", + "mention_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "message_mention_type_check": { + "name": "message_mention_type_check", + "value": "\"message_mentions\".\"mention_type\" in ('actor','room','everyone')" + } + }, + "isRLSEnabled": false + }, + "public.message_pins": { + "name": "message_pins", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pinned_by_actor_id": { + "name": "pinned_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_pins_org_room_idx": { + "name": "message_pins_org_room_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_pins_organisation_id_organisations_id_fk": { + "name": "message_pins_organisation_id_organisations_id_fk", + "tableFrom": "message_pins", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_pins_room_id_rooms_id_fk": { + "name": "message_pins_room_id_rooms_id_fk", + "tableFrom": "message_pins", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_pins_message_id_messages_id_fk": { + "name": "message_pins_message_id_messages_id_fk", + "tableFrom": "message_pins", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_pins_pinned_by_actor_id_actors_id_fk": { + "name": "message_pins_pinned_by_actor_id_actors_id_fk", + "tableFrom": "message_pins", + "tableTo": "actors", + "columnsFrom": [ + "pinned_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_pins_room_id_message_id_pk": { + "name": "message_pins_room_id_message_id_pk", + "columns": [ + "room_id", + "message_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_revisions": { + "name": "message_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_type": { + "name": "revision_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previous_document": { + "name": "previous_document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "previous_plain_text": { + "name": "previous_plain_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "next_document": { + "name": "next_document", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_plain_text": { + "name": "next_plain_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_revisions_org_message_idx": { + "name": "message_revisions_org_message_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_revisions_org_idempotency_unique": { + "name": "message_revisions_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_revisions_organisation_id_organisations_id_fk": { + "name": "message_revisions_organisation_id_organisations_id_fk", + "tableFrom": "message_revisions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_revisions_message_id_messages_id_fk": { + "name": "message_revisions_message_id_messages_id_fk", + "tableFrom": "message_revisions", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_revisions_actor_id_actors_id_fk": { + "name": "message_revisions_actor_id_actors_id_fk", + "tableFrom": "message_revisions", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "message_revision_type_check": { + "name": "message_revision_type_check", + "value": "\"message_revisions\".\"revision_type\" in ('edit','delete')" + } + }, + "isRLSEnabled": false + }, + "public.message_saves": { + "name": "message_saves", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_saves_org_actor_idx": { + "name": "message_saves_org_actor_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_saves_organisation_id_organisations_id_fk": { + "name": "message_saves_organisation_id_organisations_id_fk", + "tableFrom": "message_saves", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_saves_message_id_messages_id_fk": { + "name": "message_saves_message_id_messages_id_fk", + "tableFrom": "message_saves", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_saves_actor_id_actors_id_fk": { + "name": "message_saves_actor_id_actors_id_fk", + "tableFrom": "message_saves", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_saves_message_id_actor_id_pk": { + "name": "message_saves_message_id_actor_id_pk", + "columns": [ + "message_id", + "actor_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "thread_parent_id": { + "name": "thread_parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_actor_id": { + "name": "author_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_type": { + "name": "message_type", + "type": "message_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "plain_text": { + "name": "plain_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "edited_at": { + "name": "edited_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "data_classification": { + "name": "data_classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "related_alert_id": { + "name": "related_alert_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_investigation_id": { + "name": "related_investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_case_id": { + "name": "related_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_agent_run_id": { + "name": "related_agent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_workflow_run_id": { + "name": "related_workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "messages_org_room_time_idx": { + "name": "messages_org_room_time_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_thread_idx": { + "name": "messages_thread_idx", + "columns": [ + { + "expression": "thread_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_org_idempotency_unique": { + "name": "messages_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"messages\".\"idempotency_key\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_search_idx": { + "name": "messages_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"plain_text\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "messages_organisation_id_organisations_id_fk": { + "name": "messages_organisation_id_organisations_id_fk", + "tableFrom": "messages", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "messages_room_id_rooms_id_fk": { + "name": "messages_room_id_rooms_id_fk", + "tableFrom": "messages", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "messages_author_actor_id_actors_id_fk": { + "name": "messages_author_actor_id_actors_id_fk", + "tableFrom": "messages", + "tableTo": "actors", + "columnsFrom": [ + "author_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "messages_related_alert_id_alerts_id_fk": { + "name": "messages_related_alert_id_alerts_id_fk", + "tableFrom": "messages", + "tableTo": "alerts", + "columnsFrom": [ + "related_alert_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "messages_related_investigation_id_investigations_id_fk": { + "name": "messages_related_investigation_id_investigations_id_fk", + "tableFrom": "messages", + "tableTo": "investigations", + "columnsFrom": [ + "related_investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "safe_preview": { + "name": "safe_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_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": { + "notifications_org_actor_read_idx": { + "name": "notifications_org_actor_read_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_organisation_id_organisations_id_fk": { + "name": "notifications_organisation_id_organisations_id_fk", + "tableFrom": "notifications", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notifications_actor_id_actors_id_fk": { + "name": "notifications_actor_id_actors_id_fk", + "tableFrom": "notifications", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organisations": { + "name": "organisations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data_region": { + "name": "data_region", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'australia'" + }, + "default_timezone": { + "name": "default_timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Australia/Sydney'" + }, + "retention_policy": { + "name": "retention_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "authentication_policy": { + "name": "authentication_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organisations_slug_unique": { + "name": "organisations_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organisations_status_check": { + "name": "organisations_status_check", + "value": "\"organisations\".\"status\" in ('active','suspended')" + } + }, + "isRLSEnabled": false + }, + "public.outbox_events": { + "name": "outbox_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aggregate_type": { + "name": "aggregate_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aggregate_id": { + "name": "aggregate_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queue_name": { + "name": "queue_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "outbox_idempotency_unique": { + "name": "outbox_idempotency_unique", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_pending_idx": { + "name": "outbox_pending_idx", + "columns": [ + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"outbox_events\".\"dispatched_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "outbox_events_organisation_id_organisations_id_fk": { + "name": "outbox_events_organisation_id_organisations_id_fk", + "tableFrom": "outbox_events", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reaction_operations": { + "name": "reaction_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reaction_operations_org_idempotency_unique": { + "name": "reaction_operations_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_operations_org_message_idx": { + "name": "reaction_operations_org_message_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_operations_organisation_id_organisations_id_fk": { + "name": "reaction_operations_organisation_id_organisations_id_fk", + "tableFrom": "reaction_operations", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_operations_message_id_messages_id_fk": { + "name": "reaction_operations_message_id_messages_id_fk", + "tableFrom": "reaction_operations", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_operations_actor_id_actors_id_fk": { + "name": "reaction_operations_actor_id_actors_id_fk", + "tableFrom": "reaction_operations", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reaction_pack_assets": { + "name": "reaction_pack_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_id": { + "name": "revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "frame_count": { + "name": "frame_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_state": { + "name": "verification_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'verified'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reaction_pack_assets_org_revision_name_unique": { + "name": "reaction_pack_assets_org_revision_name_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_pack_assets_org_digest_idx": { + "name": "reaction_pack_assets_org_digest_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_pack_assets_organisation_id_organisations_id_fk": { + "name": "reaction_pack_assets_organisation_id_organisations_id_fk", + "tableFrom": "reaction_pack_assets", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_pack_assets_revision_id_reaction_pack_revisions_id_fk": { + "name": "reaction_pack_assets_revision_id_reaction_pack_revisions_id_fk", + "tableFrom": "reaction_pack_assets", + "tableTo": "reaction_pack_revisions", + "columnsFrom": [ + "revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reaction_pack_assets_verification_check": { + "name": "reaction_pack_assets_verification_check", + "value": "\"reaction_pack_assets\".\"verification_state\" in ('verified','missing','mismatch')" + }, + "reaction_pack_assets_dimensions_check": { + "name": "reaction_pack_assets_dimensions_check", + "value": "\"reaction_pack_assets\".\"width\" > 0 and \"reaction_pack_assets\".\"height\" > 0 and \"reaction_pack_assets\".\"frame_count\" > 0 and \"reaction_pack_assets\".\"byte_size\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.reaction_pack_revisions": { + "name": "reaction_pack_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_actor_id": { + "name": "approved_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "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": { + "reaction_pack_revisions_org_pack_revision_unique": { + "name": "reaction_pack_revisions_org_pack_revision_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pack_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_pack_revisions_org_status_idx": { + "name": "reaction_pack_revisions_org_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_pack_revisions_organisation_id_organisations_id_fk": { + "name": "reaction_pack_revisions_organisation_id_organisations_id_fk", + "tableFrom": "reaction_pack_revisions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_pack_revisions_pack_id_reaction_packs_id_fk": { + "name": "reaction_pack_revisions_pack_id_reaction_packs_id_fk", + "tableFrom": "reaction_pack_revisions", + "tableTo": "reaction_packs", + "columnsFrom": [ + "pack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_pack_revisions_approved_by_actor_id_actors_id_fk": { + "name": "reaction_pack_revisions_approved_by_actor_id_actors_id_fk", + "tableFrom": "reaction_pack_revisions", + "tableTo": "actors", + "columnsFrom": [ + "approved_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_pack_revisions_created_by_actor_id_actors_id_fk": { + "name": "reaction_pack_revisions_created_by_actor_id_actors_id_fk", + "tableFrom": "reaction_pack_revisions", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reaction_pack_revisions_status_check": { + "name": "reaction_pack_revisions_status_check", + "value": "\"reaction_pack_revisions\".\"status\" in ('draft','approved','superseded','removed')" + }, + "reaction_pack_revisions_revision_check": { + "name": "reaction_pack_revisions_revision_check", + "value": "\"reaction_pack_revisions\".\"revision\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.reaction_packs": { + "name": "reaction_packs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "removed_by_actor_id": { + "name": "removed_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reaction_packs_org_slug_unique": { + "name": "reaction_packs_org_slug_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_packs_org_lifecycle_idx": { + "name": "reaction_packs_org_lifecycle_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lifecycle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_packs_organisation_id_organisations_id_fk": { + "name": "reaction_packs_organisation_id_organisations_id_fk", + "tableFrom": "reaction_packs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_packs_created_by_actor_id_actors_id_fk": { + "name": "reaction_packs_created_by_actor_id_actors_id_fk", + "tableFrom": "reaction_packs", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_packs_removed_by_actor_id_actors_id_fk": { + "name": "reaction_packs_removed_by_actor_id_actors_id_fk", + "tableFrom": "reaction_packs", + "tableTo": "actors", + "columnsFrom": [ + "removed_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reaction_packs_lifecycle_check": { + "name": "reaction_packs_lifecycle_check", + "value": "\"reaction_packs\".\"lifecycle\" in ('active','removed')" + } + }, + "isRLSEnabled": false + }, + "public.reactions": { + "name": "reactions", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reactions_organisation_id_organisations_id_fk": { + "name": "reactions_organisation_id_organisations_id_fk", + "tableFrom": "reactions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reactions_message_id_messages_id_fk": { + "name": "reactions_message_id_messages_id_fk", + "tableFrom": "reactions", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reactions_actor_id_actors_id_fk": { + "name": "reactions_actor_id_actors_id_fk", + "tableFrom": "reactions", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reactions_message_id_actor_id_emoji_pk": { + "name": "reactions_message_id_actor_id_emoji_pk", + "columns": [ + "message_id", + "actor_id", + "emoji" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.report_deliveries": { + "name": "report_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "report_id": { + "name": "report_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient": { + "name": "recipient", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'awaiting_approval'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "report_deliveries_org_idempotency_unique": { + "name": "report_deliveries_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "report_deliveries_org_report_status_idx": { + "name": "report_deliveries_org_report_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "report_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "report_deliveries_organisation_id_organisations_id_fk": { + "name": "report_deliveries_organisation_id_organisations_id_fk", + "tableFrom": "report_deliveries", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_deliveries_report_id_report_manifests_id_fk": { + "name": "report_deliveries_report_id_report_manifests_id_fk", + "tableFrom": "report_deliveries", + "tableTo": "report_manifests", + "columnsFrom": [ + "report_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_deliveries_approval_id_approvals_id_fk": { + "name": "report_deliveries_approval_id_approvals_id_fk", + "tableFrom": "report_deliveries", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_deliveries_requested_by_actor_id_actors_id_fk": { + "name": "report_deliveries_requested_by_actor_id_actors_id_fk", + "tableFrom": "report_deliveries", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "report_deliveries_status_check": { + "name": "report_deliveries_status_check", + "value": "\"report_deliveries\".\"status\" in ('awaiting_approval','queued','delivered','failed','cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.report_manifests": { + "name": "report_manifests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "review_note": { + "name": "review_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "posted_message_id": { + "name": "posted_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "report_manifests_org_idempotency_unique": { + "name": "report_manifests_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "report_manifests_org_room_status_idx": { + "name": "report_manifests_org_room_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_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": { + "report_manifests_organisation_id_organisations_id_fk": { + "name": "report_manifests_organisation_id_organisations_id_fk", + "tableFrom": "report_manifests", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_agent_run_id_agent_runs_id_fk": { + "name": "report_manifests_agent_run_id_agent_runs_id_fk", + "tableFrom": "report_manifests", + "tableTo": "agent_runs", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_task_id_tasks_id_fk": { + "name": "report_manifests_task_id_tasks_id_fk", + "tableFrom": "report_manifests", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_room_id_rooms_id_fk": { + "name": "report_manifests_room_id_rooms_id_fk", + "tableFrom": "report_manifests", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_requested_by_actor_id_actors_id_fk": { + "name": "report_manifests_requested_by_actor_id_actors_id_fk", + "tableFrom": "report_manifests", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_posted_message_id_messages_id_fk": { + "name": "report_manifests_posted_message_id_messages_id_fk", + "tableFrom": "report_manifests", + "tableTo": "messages", + "columnsFrom": [ + "posted_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "report_manifests_status_check": { + "name": "report_manifests_status_check", + "value": "\"report_manifests\".\"status\" in ('draft','reviewed','posted','superseded')" + }, + "report_manifests_version_check": { + "name": "report_manifests_version_check", + "value": "\"report_manifests\".\"version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.report_schedules": { + "name": "report_schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'leadership'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "report_schedules_org_idempotency_unique": { + "name": "report_schedules_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "report_schedules_org_due_idx": { + "name": "report_schedules_org_due_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "report_schedules_organisation_id_organisations_id_fk": { + "name": "report_schedules_organisation_id_organisations_id_fk", + "tableFrom": "report_schedules", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_schedules_room_id_rooms_id_fk": { + "name": "report_schedules_room_id_rooms_id_fk", + "tableFrom": "report_schedules", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_schedules_created_by_actor_id_actors_id_fk": { + "name": "report_schedules_created_by_actor_id_actors_id_fk", + "tableFrom": "report_schedules", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "report_schedules_cadence_check": { + "name": "report_schedules_cadence_check", + "value": "\"report_schedules\".\"cadence\" in ('weekly','monthly')" + }, + "report_schedules_audience_check": { + "name": "report_schedules_audience_check", + "value": "\"report_schedules\".\"audience\" in ('analyst','leadership','executive')" + } + }, + "isRLSEnabled": false + }, + "public.research_items": { + "name": "research_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "watchlist_id": { + "name": "watchlist_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "research_run_id": { + "name": "research_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_published_at": { + "name": "source_published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "root_message_id": { + "name": "root_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_message_id": { + "name": "latest_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "brief": { + "name": "brief", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_by_actor_id": { + "name": "feedback_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "feedback_at": { + "name": "feedback_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "research_items_org_fingerprint_unique": { + "name": "research_items_org_fingerprint_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_items_org_watchlist_idx": { + "name": "research_items_org_watchlist_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "watchlist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "research_items_organisation_id_organisations_id_fk": { + "name": "research_items_organisation_id_organisations_id_fk", + "tableFrom": "research_items", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_watchlist_id_research_watchlists_id_fk": { + "name": "research_items_watchlist_id_research_watchlists_id_fk", + "tableFrom": "research_items", + "tableTo": "research_watchlists", + "columnsFrom": [ + "watchlist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_research_run_id_research_runs_id_fk": { + "name": "research_items_research_run_id_research_runs_id_fk", + "tableFrom": "research_items", + "tableTo": "research_runs", + "columnsFrom": [ + "research_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_root_message_id_messages_id_fk": { + "name": "research_items_root_message_id_messages_id_fk", + "tableFrom": "research_items", + "tableTo": "messages", + "columnsFrom": [ + "root_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_latest_message_id_messages_id_fk": { + "name": "research_items_latest_message_id_messages_id_fk", + "tableFrom": "research_items", + "tableTo": "messages", + "columnsFrom": [ + "latest_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_feedback_by_actor_id_actors_id_fk": { + "name": "research_items_feedback_by_actor_id_actors_id_fk", + "tableFrom": "research_items", + "tableTo": "actors", + "columnsFrom": [ + "feedback_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.research_runs": { + "name": "research_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "watchlist_id": { + "name": "watchlist_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "source_limit": { + "name": "source_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_budget": { + "name": "token_budget", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost_limit_cents": { + "name": "cost_limit_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "time_limit_seconds": { + "name": "time_limit_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "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": { + "research_runs_org_idempotency_unique": { + "name": "research_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_runs_org_agent_unique": { + "name": "research_runs_org_agent_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_runs_org_status_idx": { + "name": "research_runs_org_status_idx", + "columns": [ + { + "expression": "organisation_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": { + "research_runs_organisation_id_organisations_id_fk": { + "name": "research_runs_organisation_id_organisations_id_fk", + "tableFrom": "research_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_runs_watchlist_id_research_watchlists_id_fk": { + "name": "research_runs_watchlist_id_research_watchlists_id_fk", + "tableFrom": "research_runs", + "tableTo": "research_watchlists", + "columnsFrom": [ + "watchlist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_runs_agent_run_id_agent_runs_id_fk": { + "name": "research_runs_agent_run_id_agent_runs_id_fk", + "tableFrom": "research_runs", + "tableTo": "agent_runs", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "research_runs_status_check": { + "name": "research_runs_status_check", + "value": "\"research_runs\".\"status\" in ('queued','running','completed','failed')" + } + }, + "isRLSEnabled": false + }, + "public.research_watchlists": { + "name": "research_watchlists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vendors": { + "name": "vendors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "technologies": { + "name": "technologies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cadence_minutes": { + "name": "cadence_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 240 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "research_watchlists_org_name_unique": { + "name": "research_watchlists_org_name_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_watchlists_due_idx": { + "name": "research_watchlists_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "research_watchlists_organisation_id_organisations_id_fk": { + "name": "research_watchlists_organisation_id_organisations_id_fk", + "tableFrom": "research_watchlists", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_watchlists_room_id_rooms_id_fk": { + "name": "research_watchlists_room_id_rooms_id_fk", + "tableFrom": "research_watchlists", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_watchlists_created_by_actor_id_actors_id_fk": { + "name": "research_watchlists_created_by_actor_id_actors_id_fk", + "tableFrom": "research_watchlists", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "research_watchlists_cadence_check": { + "name": "research_watchlists_cadence_check", + "value": "\"research_watchlists\".\"cadence_minutes\" between 15 and 10080" + } + }, + "isRLSEnabled": false + }, + "public.room_integration_bindings": { + "name": "room_integration_bindings", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "room_integration_bindings_org_room_idx": { + "name": "room_integration_bindings_org_room_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "room_integration_bindings_organisation_id_organisations_id_fk": { + "name": "room_integration_bindings_organisation_id_organisations_id_fk", + "tableFrom": "room_integration_bindings", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_integration_bindings_room_id_rooms_id_fk": { + "name": "room_integration_bindings_room_id_rooms_id_fk", + "tableFrom": "room_integration_bindings", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_integration_bindings_integration_id_integration_records_id_fk": { + "name": "room_integration_bindings_integration_id_integration_records_id_fk", + "tableFrom": "room_integration_bindings", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_integration_bindings_created_by_actor_id_actors_id_fk": { + "name": "room_integration_bindings_created_by_actor_id_actors_id_fk", + "tableFrom": "room_integration_bindings", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "room_integration_bindings_room_id_integration_id_pk": { + "name": "room_integration_bindings_room_id_integration_id_pk", + "columns": [ + "room_id", + "integration_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.room_invitations": { + "name": "room_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invited_actor_id": { + "name": "invited_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_expires_at": { + "name": "access_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by_actor_id": { + "name": "invited_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "responded_at": { + "name": "responded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "room_invitations_org_idempotency_unique": { + "name": "room_invitations_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "room_invitations_org_room_status_idx": { + "name": "room_invitations_org_room_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "room_invitations_organisation_id_organisations_id_fk": { + "name": "room_invitations_organisation_id_organisations_id_fk", + "tableFrom": "room_invitations", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_invitations_room_id_rooms_id_fk": { + "name": "room_invitations_room_id_rooms_id_fk", + "tableFrom": "room_invitations", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_invitations_invited_actor_id_actors_id_fk": { + "name": "room_invitations_invited_actor_id_actors_id_fk", + "tableFrom": "room_invitations", + "tableTo": "actors", + "columnsFrom": [ + "invited_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_invitations_invited_by_actor_id_actors_id_fk": { + "name": "room_invitations_invited_by_actor_id_actors_id_fk", + "tableFrom": "room_invitations", + "tableTo": "actors", + "columnsFrom": [ + "invited_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "room_invitation_role_check": { + "name": "room_invitation_role_check", + "value": "\"room_invitations\".\"membership_role\" in ('moderator','member','guest','agent_member')" + }, + "room_invitation_status_check": { + "name": "room_invitation_status_check", + "value": "\"room_invitations\".\"status\" in ('pending','accepted','revoked','expired')" + } + }, + "isRLSEnabled": false + }, + "public.room_memberships": { + "name": "room_memberships", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_level": { + "name": "notification_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "notify_replies": { + "name": "notify_replies", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_followed_threads": { + "name": "notify_followed_threads", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_read_event_id": { + "name": "last_read_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "muted": { + "name": "muted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "favourite": { + "name": "favourite", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sidebar_position": { + "name": "sidebar_position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sidebar_group": { + "name": "sidebar_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_expires_at": { + "name": "access_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "room_memberships_org_actor_idx": { + "name": "room_memberships_org_actor_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "room_memberships_organisation_id_organisations_id_fk": { + "name": "room_memberships_organisation_id_organisations_id_fk", + "tableFrom": "room_memberships", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_memberships_room_id_rooms_id_fk": { + "name": "room_memberships_room_id_rooms_id_fk", + "tableFrom": "room_memberships", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_memberships_actor_id_actors_id_fk": { + "name": "room_memberships_actor_id_actors_id_fk", + "tableFrom": "room_memberships", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "room_memberships_room_id_actor_id_pk": { + "name": "room_memberships_room_id_actor_id_pk", + "columns": [ + "room_id", + "actor_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "room_membership_role_check": { + "name": "room_membership_role_check", + "value": "\"room_memberships\".\"membership_role\" in ('owner','moderator','member','guest','agent_member')" + } + }, + "isRLSEnabled": false + }, + "public.rooms": { + "name": "rooms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "room_type": { + "name": "room_type", + "type": "room_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'organisation'" + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "policies": { + "name": "policies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "direct_fingerprint": { + "name": "direct_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "linked_investigation_id": { + "name": "linked_investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_kelpie_case_id": { + "name": "linked_kelpie_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_severity": { + "name": "default_severity", + "type": "severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "tlp": { + "name": "tlp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'amber'" + }, + "retention_policy": { + "name": "retention_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "rooms_org_slug_unique": { + "name": "rooms_org_slug_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rooms_org_direct_fingerprint_unique": { + "name": "rooms_org_direct_fingerprint_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direct_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"rooms\".\"direct_fingerprint\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "rooms_org_type_idx": { + "name": "rooms_org_type_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rooms_organisation_id_organisations_id_fk": { + "name": "rooms_organisation_id_organisations_id_fk", + "tableFrom": "rooms", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rooms_created_by_actor_id_actors_id_fk": { + "name": "rooms_created_by_actor_id_actors_id_fk", + "tableFrom": "rooms", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.synthetic_cleanup_receipts": { + "name": "synthetic_cleanup_receipts", + "schema": "", + "columns": { + "manifest_id": { + "name": "manifest_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "maintenance_actor_id": { + "name": "maintenance_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "manifest_digest": { + "name": "manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "candidate_counts": { + "name": "candidate_counts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pre_digests": { + "name": "pre_digests", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "post_digests": { + "name": "post_digests", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "object_storage_objects": { + "name": "object_storage_objects", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "synthetic_cleanup_receipts_approval_unique": { + "name": "synthetic_cleanup_receipts_approval_unique", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "synthetic_cleanup_receipts_org_digest_unique": { + "name": "synthetic_cleanup_receipts_org_digest_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "manifest_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "synthetic_cleanup_receipts_organisation_id_organisations_id_fk": { + "name": "synthetic_cleanup_receipts_organisation_id_organisations_id_fk", + "tableFrom": "synthetic_cleanup_receipts", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "synthetic_cleanup_receipts_approval_id_approvals_id_fk": { + "name": "synthetic_cleanup_receipts_approval_id_approvals_id_fk", + "tableFrom": "synthetic_cleanup_receipts", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "synthetic_cleanup_receipts_maintenance_actor_id_actors_id_fk": { + "name": "synthetic_cleanup_receipts_maintenance_actor_id_actors_id_fk", + "tableFrom": "synthetic_cleanup_receipts", + "tableTo": "actors", + "columnsFrom": [ + "maintenance_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "priority": { + "name": "priority", + "type": "task_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "assigned_actor_id": { + "name": "assigned_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_case_id": { + "name": "related_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approval_required": { + "name": "approval_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_run_status": { + "name": "agent_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_org_status_idx": { + "name": "tasks_org_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_org_assignee_idx": { + "name": "tasks_org_assignee_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assigned_actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_org_idempotency_unique": { + "name": "tasks_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_organisation_id_organisations_id_fk": { + "name": "tasks_organisation_id_organisations_id_fk", + "tableFrom": "tasks", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_assigned_actor_id_actors_id_fk": { + "name": "tasks_assigned_actor_id_actors_id_fk", + "tableFrom": "tasks", + "tableTo": "actors", + "columnsFrom": [ + "assigned_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_created_by_actor_id_actors_id_fk": { + "name": "tasks_created_by_actor_id_actors_id_fk", + "tableFrom": "tasks", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_room_id_rooms_id_fk": { + "name": "tasks_room_id_rooms_id_fk", + "tableFrom": "tasks", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_investigation_id_investigations_id_fk": { + "name": "tasks_investigation_id_investigations_id_fk", + "tableFrom": "tasks", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.thread_follows": { + "name": "thread_follows", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "root_message_id": { + "name": "root_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "thread_follows_org_actor_idx": { + "name": "thread_follows_org_actor_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "thread_follows_organisation_id_organisations_id_fk": { + "name": "thread_follows_organisation_id_organisations_id_fk", + "tableFrom": "thread_follows", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "thread_follows_root_message_id_messages_id_fk": { + "name": "thread_follows_root_message_id_messages_id_fk", + "tableFrom": "thread_follows", + "tableTo": "messages", + "columnsFrom": [ + "root_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "thread_follows_actor_id_actors_id_fk": { + "name": "thread_follows_actor_id_actors_id_fk", + "tableFrom": "thread_follows", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "thread_follows_root_message_id_actor_id_pk": { + "name": "thread_follows_root_message_id_actor_id_pk", + "columns": [ + "root_message_id", + "actor_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.timeline_events": { + "name": "timeline_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "external_case_id": { + "name": "external_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "occurred_at": { + "name": "occurred_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()" + } + }, + "indexes": { + "timeline_org_investigation_time_idx": { + "name": "timeline_org_investigation_time_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "investigation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "timeline_events_organisation_id_organisations_id_fk": { + "name": "timeline_events_organisation_id_organisations_id_fk", + "tableFrom": "timeline_events", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "timeline_events_investigation_id_investigations_id_fk": { + "name": "timeline_events_investigation_id_investigations_id_fk", + "tableFrom": "timeline_events", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "timeline_events_room_id_rooms_id_fk": { + "name": "timeline_events_room_id_rooms_id_fk", + "tableFrom": "timeline_events", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "timeline_events_actor_id_actors_id_fk": { + "name": "timeline_events_actor_id_actors_id_fk", + "tableFrom": "timeline_events", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "better_auth_user_id": { + "name": "better_auth_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team": { + "name": "team", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "presence_state": { + "name": "presence_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'offline'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Australia/Sydney'" + }, + "notification_preferences": { + "name": "notification_preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_org_email_unique": { + "name": "users_org_email_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_better_auth_unique": { + "name": "users_better_auth_unique", + "columns": [ + { + "expression": "better_auth_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_org_idx": { + "name": "users_org_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_organisation_id_organisations_id_fk": { + "name": "users_organisation_id_organisations_id_fk", + "tableFrom": "users", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_better_auth_user_id_auth_user_id_fk": { + "name": "users_better_auth_user_id_auth_user_id_fk", + "tableFrom": "users", + "tableTo": "auth_user", + "columnsFrom": [ + "better_auth_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_definitions": { + "name": "workflow_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_key": { + "name": "workflow_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "yaml": { + "name": "yaml", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parsed": { + "name": "parsed", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "owner_actor_id": { + "name": "owner_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "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": { + "workflow_defs_org_key_version_unique": { + "name": "workflow_defs_org_key_version_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_definitions_organisation_id_organisations_id_fk": { + "name": "workflow_definitions_organisation_id_organisations_id_fk", + "tableFrom": "workflow_definitions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_definitions_owner_actor_id_actors_id_fk": { + "name": "workflow_definitions_owner_actor_id_actors_id_fk", + "tableFrom": "workflow_definitions", + "tableTo": "actors", + "columnsFrom": [ + "owner_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_runs": { + "name": "workflow_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_definition_id": { + "name": "workflow_definition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "trigger_event_id": { + "name": "trigger_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workflow_runs_org_idempotency_unique": { + "name": "workflow_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_runs_organisation_id_organisations_id_fk": { + "name": "workflow_runs_organisation_id_organisations_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_runs_workflow_definition_id_workflow_definitions_id_fk": { + "name": "workflow_runs_workflow_definition_id_workflow_definitions_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "workflow_definitions", + "columnsFrom": [ + "workflow_definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_runs_room_id_rooms_id_fk": { + "name": "workflow_runs_room_id_rooms_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_runs_investigation_id_investigations_id_fk": { + "name": "workflow_runs_investigation_id_investigations_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_runs_requested_by_actor_id_actors_id_fk": { + "name": "workflow_runs_requested_by_actor_id_actors_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.actor_type": { + "name": "actor_type", + "schema": "public", + "values": [ + "human", + "agent", + "product", + "service", + "system" + ] + }, + "public.alert_status": { + "name": "alert_status", + "schema": "public", + "values": [ + "new", + "acknowledged", + "investigating", + "dismissed", + "promoted", + "closed" + ] + }, + "public.approval_status": { + "name": "approval_status", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected", + "expired", + "cancelled", + "executed", + "failed" + ] + }, + "public.investigation_status": { + "name": "investigation_status", + "schema": "public", + "values": [ + "open", + "triaging", + "investigating", + "awaiting_approval", + "promoted", + "closed" + ] + }, + "public.message_type": { + "name": "message_type", + "schema": "public", + "values": [ + "text", + "system", + "alert", + "finding", + "decision", + "approval", + "workflow", + "agent-status", + "query-result", + "evidence", + "case-event", + "response-action" + ] + }, + "public.room_type": { + "name": "room_type", + "schema": "public", + "values": [ + "operations", + "incident", + "investigation", + "hunt", + "engineering", + "private", + "direct", + "system" + ] + }, + "public.severity": { + "name": "severity", + "schema": "public", + "values": [ + "critical", + "high", + "medium", + "low", + "informational" + ] + }, + "public.task_priority": { + "name": "task_priority", + "schema": "public", + "values": [ + "urgent", + "high", + "normal", + "low" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "backlog", + "ready", + "in_progress", + "review", + "done" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/database/migrations/meta/0019_snapshot.json b/packages/database/migrations/meta/0019_snapshot.json new file mode 100644 index 0000000..b78af12 --- /dev/null +++ b/packages/database/migrations/meta/0019_snapshot.json @@ -0,0 +1,11655 @@ +{ + "id": "19f6aac0-aa0b-471b-88d8-0a1f27531db9", + "prevId": "71b9ed46-7893-4ce6-ba96-f34a03d21ed0", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.actors": { + "name": "actors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "identity_reference": { + "name": "identity_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capability_assignments": { + "name": "capability_assignments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "actors_org_type_idx": { + "name": "actors_org_type_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "actors_org_identity_unique": { + "name": "actors_org_identity_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "identity_reference", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"actors\".\"identity_reference\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "actors_organisation_id_organisations_id_fk": { + "name": "actors_organisation_id_organisations_id_fk", + "tableFrom": "actors", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_definitions": { + "name": "agent_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_actor_id": { + "name": "owner_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "system_prompt_version": { + "name": "system_prompt_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "allowed_rooms": { + "name": "allowed_rooms", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "capability_requirements": { + "name": "capability_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "maximum_runtime_seconds": { + "name": "maximum_runtime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "maximum_token_budget": { + "name": "maximum_token_budget", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20000 + }, + "maximum_cost_cents": { + "name": "maximum_cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 500 + }, + "data_classification_allowance": { + "name": "data_classification_allowance", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"internal\"]'::jsonb" + }, + "approval_requirements": { + "name": "approval_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "requested_permission_mode": { + "name": "requested_permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'read_only'" + }, + "kill_switch": { + "name": "kill_switch", + "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": { + "agent_definitions_org_name_unique": { + "name": "agent_definitions_org_name_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_definitions_organisation_id_organisations_id_fk": { + "name": "agent_definitions_organisation_id_organisations_id_fk", + "tableFrom": "agent_definitions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_definitions_owner_actor_id_actors_id_fk": { + "name": "agent_definitions_owner_actor_id_actors_id_fk", + "tableFrom": "agent_definitions", + "tableTo": "actors", + "columnsFrom": [ + "owner_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_definitions_requested_permission_check": { + "name": "agent_definitions_requested_permission_check", + "value": "\"agent_definitions\".\"requested_permission_mode\" in ('read_only','approval_gated')" + } + }, + "isRLSEnabled": false + }, + "public.agent_memories": { + "name": "agent_memories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence_references": { + "name": "evidence_references", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "supersedes_memory_id": { + "name": "supersedes_memory_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reviewed_by_actor_id": { + "name": "reviewed_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_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": { + "agent_memories_org_agent_idx": { + "name": "agent_memories_org_agent_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_memories_organisation_id_organisations_id_fk": { + "name": "agent_memories_organisation_id_organisations_id_fk", + "tableFrom": "agent_memories", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_memories_agent_id_agent_definitions_id_fk": { + "name": "agent_memories_agent_id_agent_definitions_id_fk", + "tableFrom": "agent_memories", + "tableTo": "agent_definitions", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_memories_source_run_id_agent_runs_id_fk": { + "name": "agent_memories_source_run_id_agent_runs_id_fk", + "tableFrom": "agent_memories", + "tableTo": "agent_runs", + "columnsFrom": [ + "source_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_memories_reviewed_by_actor_id_actors_id_fk": { + "name": "agent_memories_reviewed_by_actor_id_actors_id_fk", + "tableFrom": "agent_memories", + "tableTo": "actors", + "columnsFrom": [ + "reviewed_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_memories_kind_check": { + "name": "agent_memories_kind_check", + "value": "\"agent_memories\".\"kind\" in ('fact','preference','lesson','failure','procedure_hint')" + }, + "agent_memories_status_check": { + "name": "agent_memories_status_check", + "value": "\"agent_memories\".\"status\" in ('active','superseded','expired','rejected')" + }, + "agent_memories_confidence_check": { + "name": "agent_memories_confidence_check", + "value": "\"agent_memories\".\"confidence\" between 0 and 100" + } + }, + "isRLSEnabled": false + }, + "public.agent_readiness_snapshots": { + "name": "agent_readiness_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "process_identity": { + "name": "process_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gateway_state": { + "name": "gateway_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authentication_state": { + "name": "authentication_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observer_state": { + "name": "observer_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle_evidence_state": { + "name": "lifecycle_evidence_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle_state": { + "name": "lifecycle_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capability_state": { + "name": "capability_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_state": { + "name": "tool_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_state": { + "name": "permission_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_runtime": { + "name": "reported_runtime", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_provider": { + "name": "reported_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_model": { + "name": "reported_model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_capabilities": { + "name": "input_capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "output_capabilities": { + "name": "output_capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "available_commands": { + "name": "available_commands", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "tool_sources": { + "name": "tool_sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "tool_risk_classes": { + "name": "tool_risk_classes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "requested_permission_mode": { + "name": "requested_permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_permission_mode": { + "name": "effective_permission_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limitations": { + "name": "limitations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_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()" + } + }, + "indexes": { + "agent_readiness_org_agent_verified_idx": { + "name": "agent_readiness_org_agent_verified_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verified_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_readiness_org_process_verified_idx": { + "name": "agent_readiness_org_process_verified_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "process_identity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verified_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_readiness_snapshots_organisation_id_organisations_id_fk": { + "name": "agent_readiness_snapshots_organisation_id_organisations_id_fk", + "tableFrom": "agent_readiness_snapshots", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_readiness_snapshots_agent_id_agent_definitions_id_fk": { + "name": "agent_readiness_snapshots_agent_id_agent_definitions_id_fk", + "tableFrom": "agent_readiness_snapshots", + "tableTo": "agent_definitions", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_readiness_evidence_states_check": { + "name": "agent_readiness_evidence_states_check", + "value": "\"agent_readiness_snapshots\".\"gateway_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"authentication_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"observer_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"lifecycle_evidence_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"capability_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"tool_state\" in ('reported','unavailable','unknown')\n and \"agent_readiness_snapshots\".\"permission_state\" in ('reported','unavailable','unknown')" + }, + "agent_readiness_lifecycle_state_check": { + "name": "agent_readiness_lifecycle_state_check", + "value": "\"agent_readiness_snapshots\".\"lifecycle_state\" in ('idle','running','stopped','failed','unknown')" + }, + "agent_readiness_permission_modes_check": { + "name": "agent_readiness_permission_modes_check", + "value": "\"agent_readiness_snapshots\".\"requested_permission_mode\" in ('read_only','approval_gated','unknown')\n and \"agent_readiness_snapshots\".\"effective_permission_mode\" in ('read_only','approval_gated','unknown')" + } + }, + "isRLSEnabled": false + }, + "public.agent_run_events": { + "name": "agent_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_run_events_org_run_idx": { + "name": "agent_run_events_org_run_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_events_organisation_id_organisations_id_fk": { + "name": "agent_run_events_organisation_id_organisations_id_fk", + "tableFrom": "agent_run_events", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_run_events_run_id_agent_runs_id_fk": { + "name": "agent_run_events_run_id_agent_runs_id_fk", + "tableFrom": "agent_run_events", + "tableTo": "agent_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_run_sources": { + "name": "agent_run_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_run_sources_org_run_unique": { + "name": "agent_run_sources_org_run_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_run_sources_org_run_idx": { + "name": "agent_run_sources_org_run_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_run_sources_organisation_id_organisations_id_fk": { + "name": "agent_run_sources_organisation_id_organisations_id_fk", + "tableFrom": "agent_run_sources", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_run_sources_run_id_agent_runs_id_fk": { + "name": "agent_run_sources_run_id_agent_runs_id_fk", + "tableFrom": "agent_run_sources", + "tableTo": "agent_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runs": { + "name": "agent_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workflow_run_id": { + "name": "workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "progress": { + "name": "progress", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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 + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deadline_at": { + "name": "deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "worker_id": { + "name": "worker_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt_hash": { + "name": "prompt_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_hash": { + "name": "output_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output_schema": { + "name": "output_schema", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt_version": { + "name": "prompt_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "maximum_runtime_seconds": { + "name": "maximum_runtime_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "maximum_token_budget": { + "name": "maximum_token_budget", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20000 + }, + "maximum_cost_cents": { + "name": "maximum_cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 500 + }, + "token_usage": { + "name": "token_usage", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "estimated_cost_cents": { + "name": "estimated_cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_call_count": { + "name": "tool_call_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "diagnostics": { + "name": "diagnostics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "structured_output": { + "name": "structured_output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "agent_runs_org_idempotency_unique": { + "name": "agent_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runs_org_status_idx": { + "name": "agent_runs_org_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runs_recovery_idx": { + "name": "agent_runs_recovery_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runs_agent_id_agent_definitions_id_fk": { + "name": "agent_runs_agent_id_agent_definitions_id_fk", + "tableFrom": "agent_runs", + "tableTo": "agent_definitions", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runs_organisation_id_organisations_id_fk": { + "name": "agent_runs_organisation_id_organisations_id_fk", + "tableFrom": "agent_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runs_room_id_rooms_id_fk": { + "name": "agent_runs_room_id_rooms_id_fk", + "tableFrom": "agent_runs", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runs_investigation_id_investigations_id_fk": { + "name": "agent_runs_investigation_id_investigations_id_fk", + "tableFrom": "agent_runs", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runs_requested_by_actor_id_actors_id_fk": { + "name": "agent_runs_requested_by_actor_id_actors_id_fk", + "tableFrom": "agent_runs", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_skill_evaluations": { + "name": "agent_skill_evaluations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_version_id": { + "name": "skill_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "evaluator_actor_id": { + "name": "evaluator_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "suite": { + "name": "suite", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "passed": { + "name": "passed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "baseline_score": { + "name": "baseline_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "regressions": { + "name": "regressions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_skill_evaluations_version_idx": { + "name": "agent_skill_evaluations_version_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_skill_evaluations_organisation_id_organisations_id_fk": { + "name": "agent_skill_evaluations_organisation_id_organisations_id_fk", + "tableFrom": "agent_skill_evaluations", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_evaluations_skill_version_id_agent_skill_versions_id_fk": { + "name": "agent_skill_evaluations_skill_version_id_agent_skill_versions_id_fk", + "tableFrom": "agent_skill_evaluations", + "tableTo": "agent_skill_versions", + "columnsFrom": [ + "skill_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_evaluations_evaluator_actor_id_actors_id_fk": { + "name": "agent_skill_evaluations_evaluator_actor_id_actors_id_fk", + "tableFrom": "agent_skill_evaluations", + "tableTo": "actors", + "columnsFrom": [ + "evaluator_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_skill_evaluations_score_check": { + "name": "agent_skill_evaluations_score_check", + "value": "\"agent_skill_evaluations\".\"score\" between 0 and 100" + } + }, + "isRLSEnabled": false + }, + "public.agent_skill_versions": { + "name": "agent_skill_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "based_on_version_id": { + "name": "based_on_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_rationale": { + "name": "change_rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence_references": { + "name": "evidence_references", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "required_capabilities": { + "name": "required_capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "allowed_tools": { + "name": "allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'proposed'" + }, + "approved_by_actor_id": { + "name": "approved_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_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": { + "agent_skill_versions_skill_version_unique": { + "name": "agent_skill_versions_skill_version_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_skill_versions_content_hash_unique": { + "name": "agent_skill_versions_content_hash_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "content_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_skill_versions_organisation_id_organisations_id_fk": { + "name": "agent_skill_versions_organisation_id_organisations_id_fk", + "tableFrom": "agent_skill_versions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_versions_skill_id_agent_skills_id_fk": { + "name": "agent_skill_versions_skill_id_agent_skills_id_fk", + "tableFrom": "agent_skill_versions", + "tableTo": "agent_skills", + "columnsFrom": [ + "skill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_versions_source_run_id_agent_runs_id_fk": { + "name": "agent_skill_versions_source_run_id_agent_runs_id_fk", + "tableFrom": "agent_skill_versions", + "tableTo": "agent_runs", + "columnsFrom": [ + "source_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skill_versions_approved_by_actor_id_actors_id_fk": { + "name": "agent_skill_versions_approved_by_actor_id_actors_id_fk", + "tableFrom": "agent_skill_versions", + "tableTo": "actors", + "columnsFrom": [ + "approved_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_skill_versions_state_check": { + "name": "agent_skill_versions_state_check", + "value": "\"agent_skill_versions\".\"state\" in ('proposed','evaluating','approved','rejected','published','rolled_back')" + } + }, + "isRLSEnabled": false + }, + "public.agent_skills": { + "name": "agent_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "skill_key": { + "name": "skill_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "active_version_id": { + "name": "active_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "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": { + "agent_skills_org_agent_key_unique": { + "name": "agent_skills_org_agent_key_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "skill_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_skills_organisation_id_organisations_id_fk": { + "name": "agent_skills_organisation_id_organisations_id_fk", + "tableFrom": "agent_skills", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skills_agent_id_agent_definitions_id_fk": { + "name": "agent_skills_agent_id_agent_definitions_id_fk", + "tableFrom": "agent_skills", + "tableTo": "agent_definitions", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_skills_created_by_actor_id_actors_id_fk": { + "name": "agent_skills_created_by_actor_id_actors_id_fk", + "tableFrom": "agent_skills", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_skills_status_check": { + "name": "agent_skills_status_check", + "value": "\"agent_skills\".\"status\" in ('draft','evaluating','published','retired')" + } + }, + "isRLSEnabled": false + }, + "public.agent_tool_calls": { + "name": "agent_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "arguments_hash": { + "name": "arguments_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result_hash": { + "name": "result_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_tool_calls_org_run_idx": { + "name": "agent_tool_calls_org_run_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_tool_calls_organisation_id_organisations_id_fk": { + "name": "agent_tool_calls_organisation_id_organisations_id_fk", + "tableFrom": "agent_tool_calls", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_tool_calls_run_id_agent_runs_id_fk": { + "name": "agent_tool_calls_run_id_agent_runs_id_fk", + "tableFrom": "agent_tool_calls", + "tableTo": "agent_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_tool_calls_approval_id_approvals_id_fk": { + "name": "agent_tool_calls_approval_id_approvals_id_fk", + "tableFrom": "agent_tool_calls", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alerts": { + "name": "alerts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_product": { + "name": "source_product", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_instance": { + "name": "source_instance", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_reference": { + "name": "external_reference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "severity": { + "name": "severity", + "type": "severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "alert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'new'" + }, + "rule_name": { + "name": "rule_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "assigned_actor_id": { + "name": "assigned_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "entities": { + "name": "entities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "observables": { + "name": "observables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "raw_reference_metadata": { + "name": "raw_reference_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kelpie_case_id": { + "name": "kelpie_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "correlation_key": { + "name": "correlation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "alerts_org_source_ref_unique": { + "name": "alerts_org_source_ref_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_instance", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_reference", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alerts_org_dedupe_unique": { + "name": "alerts_org_dedupe_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alerts_org_queue_idx": { + "name": "alerts_org_queue_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alerts_search_idx": { + "name": "alerts_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"title\" || ' ' || \"description\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "alerts_organisation_id_organisations_id_fk": { + "name": "alerts_organisation_id_organisations_id_fk", + "tableFrom": "alerts", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "alerts_assigned_actor_id_actors_id_fk": { + "name": "alerts_assigned_actor_id_actors_id_fk", + "tableFrom": "alerts", + "tableTo": "actors", + "columnsFrom": [ + "assigned_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "alerts_room_id_rooms_id_fk": { + "name": "alerts_room_id_rooms_id_fk", + "tableFrom": "alerts", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requesting_actor_id": { + "name": "requesting_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "risk_summary": { + "name": "risk_summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "required_capability": { + "name": "required_capability", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "required_approval_count": { + "name": "required_approval_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "decisions": { + "name": "decisions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "decision_at": { + "name": "decision_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "approvals_org_idempotency_unique": { + "name": "approvals_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approvals_org_status_idx": { + "name": "approvals_org_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_organisation_id_organisations_id_fk": { + "name": "approvals_organisation_id_organisations_id_fk", + "tableFrom": "approvals", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requesting_actor_id_actors_id_fk": { + "name": "approvals_requesting_actor_id_actors_id_fk", + "tableFrom": "approvals", + "tableTo": "actors", + "columnsFrom": [ + "requesting_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_events": { + "name": "audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previous_hash": { + "name": "previous_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_hash": { + "name": "event_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_org_sequence_unique": { + "name": "audit_org_sequence_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_org_hash_unique": { + "name": "audit_org_hash_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_org_target_idx": { + "name": "audit_org_target_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_events_organisation_id_organisations_id_fk": { + "name": "audit_events_organisation_id_organisations_id_fk", + "tableFrom": "audit_events", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "audit_events_actor_id_actors_id_fk": { + "name": "audit_events_actor_id_actors_id_fk", + "tableFrom": "audit_events", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_account": { + "name": "auth_account", + "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": { + "auth_accounts_user_idx": { + "name": "auth_accounts_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_accounts_provider_unique": { + "name": "auth_accounts_provider_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_account_user_id_auth_user_id_fk": { + "name": "auth_account_user_id_auth_user_id_fk", + "tableFrom": "auth_account", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_passkey": { + "name": "auth_passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_passkey_user_idx": { + "name": "auth_passkey_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_passkey_user_id_auth_user_id_fk": { + "name": "auth_passkey_user_id_auth_user_id_fk", + "tableFrom": "auth_passkey", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_passkey_credential_id_unique": { + "name": "auth_passkey_credential_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_session": { + "name": "auth_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "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 + }, + "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": { + "auth_sessions_user_idx": { + "name": "auth_sessions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_session_user_id_auth_user_id_fk": { + "name": "auth_session_user_id_auth_user_id_fk", + "tableFrom": "auth_session", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_session_token_unique": { + "name": "auth_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_two_factor": { + "name": "auth_two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_two_factor_user_unique": { + "name": "auth_two_factor_user_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_two_factor_user_id_auth_user_id_fk": { + "name": "auth_two_factor_user_id_auth_user_id_fk", + "tableFrom": "auth_two_factor", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_user": { + "name": "auth_user", + "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 + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_user_email_unique": { + "name": "auth_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_verification": { + "name": "auth_verification", + "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": { + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.decisions": { + "name": "decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decision_maker_actor_id": { + "name": "decision_maker_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "alternatives_considered": { + "name": "alternatives_considered", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "evidence_references": { + "name": "evidence_references", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "related_investigation_id": { + "name": "related_investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_case_id": { + "name": "related_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_workflow_run_id": { + "name": "related_workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "decisions_org_investigation_idx": { + "name": "decisions_org_investigation_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_investigation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "decisions_organisation_id_organisations_id_fk": { + "name": "decisions_organisation_id_organisations_id_fk", + "tableFrom": "decisions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_decision_maker_actor_id_actors_id_fk": { + "name": "decisions_decision_maker_actor_id_actors_id_fk", + "tableFrom": "decisions", + "tableTo": "actors", + "columnsFrom": [ + "decision_maker_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "decisions_related_investigation_id_investigations_id_fk": { + "name": "decisions_related_investigation_id_investigations_id_fk", + "tableFrom": "decisions", + "tableTo": "investigations", + "columnsFrom": [ + "related_investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.evidence": { + "name": "evidence", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by_actor_id": { + "name": "uploaded_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "related_room_id": { + "name": "related_room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_investigation_id": { + "name": "related_investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_case_id": { + "name": "related_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_timestamp": { + "name": "original_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scan_state": { + "name": "scan_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "retention_state": { + "name": "retention_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "legal_hold": { + "name": "legal_hold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "object_lock_metadata": { + "name": "object_lock_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "evidence_org_hash_unique": { + "name": "evidence_org_hash_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "evidence_search_idx": { + "name": "evidence_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"file_name\" || ' ' || \"source\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "evidence_organisation_id_organisations_id_fk": { + "name": "evidence_organisation_id_organisations_id_fk", + "tableFrom": "evidence", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evidence_uploaded_by_actor_id_actors_id_fk": { + "name": "evidence_uploaded_by_actor_id_actors_id_fk", + "tableFrom": "evidence", + "tableTo": "actors", + "columnsFrom": [ + "uploaded_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evidence_related_room_id_rooms_id_fk": { + "name": "evidence_related_room_id_rooms_id_fk", + "tableFrom": "evidence", + "tableTo": "rooms", + "columnsFrom": [ + "related_room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evidence_related_investigation_id_investigations_id_fk": { + "name": "evidence_related_investigation_id_investigations_id_fk", + "tableFrom": "evidence", + "tableTo": "investigations", + "columnsFrom": [ + "related_investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.findings": { + "name": "findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "supporting_evidence": { + "name": "supporting_evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "related_entities": { + "name": "related_entities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "related_observables": { + "name": "related_observables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "recommended_action": { + "name": "recommended_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_provenance": { + "name": "agent_provenance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "human_reviewed_at": { + "name": "human_reviewed_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()" + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "findings_org_investigation_idx": { + "name": "findings_org_investigation_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "investigation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "findings_search_idx": { + "name": "findings_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"title\" || ' ' || \"summary\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "findings_organisation_id_organisations_id_fk": { + "name": "findings_organisation_id_organisations_id_fk", + "tableFrom": "findings", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "findings_investigation_id_investigations_id_fk": { + "name": "findings_investigation_id_investigations_id_fk", + "tableFrom": "findings", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "findings_created_by_actor_id_actors_id_fk": { + "name": "findings_created_by_actor_id_actors_id_fk", + "tableFrom": "findings", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hunt_queries": { + "name": "hunt_queries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "hunt_id": { + "name": "hunt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "query_run_id": { + "name": "query_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "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": { + "hunt_queries_org_query_run_unique": { + "name": "hunt_queries_org_query_run_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "query_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hunt_queries_org_hunt_sequence_unique": { + "name": "hunt_queries_org_hunt_sequence_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hunt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hunt_queries_org_hunt_idx": { + "name": "hunt_queries_org_hunt_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hunt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hunt_queries_organisation_id_organisations_id_fk": { + "name": "hunt_queries_organisation_id_organisations_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_queries_hunt_id_hunt_runs_id_fk": { + "name": "hunt_queries_hunt_id_hunt_runs_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "hunt_runs", + "columnsFrom": [ + "hunt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_queries_integration_id_integration_records_id_fk": { + "name": "hunt_queries_integration_id_integration_records_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_queries_template_id_integration_query_templates_id_fk": { + "name": "hunt_queries_template_id_integration_query_templates_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "integration_query_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_queries_query_run_id_integration_query_runs_id_fk": { + "name": "hunt_queries_query_run_id_integration_query_runs_id_fk", + "tableFrom": "hunt_queries", + "tableTo": "integration_query_runs", + "columnsFrom": [ + "query_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hunt_runs": { + "name": "hunt_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_message_id": { + "name": "source_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_case_id": { + "name": "linked_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "question": { + "name": "question", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "training_mode": { + "name": "training_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "plan": { + "name": "plan", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hunt_runs_org_idempotency_unique": { + "name": "hunt_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hunt_runs_org_agent_run_unique": { + "name": "hunt_runs_org_agent_run_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hunt_runs_org_status_idx": { + "name": "hunt_runs_org_status_idx", + "columns": [ + { + "expression": "organisation_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": { + "hunt_runs_organisation_id_organisations_id_fk": { + "name": "hunt_runs_organisation_id_organisations_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_agent_run_id_agent_runs_id_fk": { + "name": "hunt_runs_agent_run_id_agent_runs_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "agent_runs", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_task_id_tasks_id_fk": { + "name": "hunt_runs_task_id_tasks_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_source_message_id_messages_id_fk": { + "name": "hunt_runs_source_message_id_messages_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "messages", + "columnsFrom": [ + "source_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_room_id_rooms_id_fk": { + "name": "hunt_runs_room_id_rooms_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_requested_by_actor_id_actors_id_fk": { + "name": "hunt_runs_requested_by_actor_id_actors_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hunt_runs_approval_id_approvals_id_fk": { + "name": "hunt_runs_approval_id_approvals_id_fk", + "tableFrom": "hunt_runs", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "hunt_runs_status_check": { + "name": "hunt_runs_status_check", + "value": "\"hunt_runs\".\"status\" in ('planned','awaiting_approval','querying','analysing','completed','failed','cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.hypotheses": { + "name": "hypotheses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "statement": { + "name": "statement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unverified'" + }, + "confidence": { + "name": "confidence", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "supporting_finding_ids": { + "name": "supporting_finding_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "contradicting_finding_ids": { + "name": "contradicting_finding_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "hypotheses_org_investigation_idx": { + "name": "hypotheses_org_investigation_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "investigation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hypotheses_organisation_id_organisations_id_fk": { + "name": "hypotheses_organisation_id_organisations_id_fk", + "tableFrom": "hypotheses", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hypotheses_investigation_id_investigations_id_fk": { + "name": "hypotheses_investigation_id_investigations_id_fk", + "tableFrom": "hypotheses", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hypotheses_created_by_actor_id_actors_id_fk": { + "name": "hypotheses_created_by_actor_id_actors_id_fk", + "tableFrom": "hypotheses", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_records": { + "name": "idempotency_records", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idempotency_expiry_idx": { + "name": "idempotency_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "idempotency_records_organisation_id_organisations_id_fk": { + "name": "idempotency_records_organisation_id_organisations_id_fk", + "tableFrom": "idempotency_records", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "idempotency_records_organisation_id_scope_key_pk": { + "name": "idempotency_records_organisation_id_scope_key_pk", + "columns": [ + "organisation_id", + "scope", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connector_credentials": { + "name": "integration_connector_credentials", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "encrypted_credential": { + "name": "encrypted_credential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "envelope_version": { + "name": "envelope_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'v1'" + }, + "rotation_version": { + "name": "rotation_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "rotated_by_actor_id": { + "name": "rotated_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_credentials_org_idx": { + "name": "integration_credentials_org_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_connector_credentials_organisation_id_organisations_id_fk": { + "name": "integration_connector_credentials_organisation_id_organisations_id_fk", + "tableFrom": "integration_connector_credentials", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_connector_credentials_integration_id_integration_records_id_fk": { + "name": "integration_connector_credentials_integration_id_integration_records_id_fk", + "tableFrom": "integration_connector_credentials", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_connector_credentials_rotated_by_actor_id_actors_id_fk": { + "name": "integration_connector_credentials_rotated_by_actor_id_actors_id_fk", + "tableFrom": "integration_connector_credentials", + "tableTo": "actors", + "columnsFrom": [ + "rotated_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_deliveries": { + "name": "integration_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_metadata": { + "name": "request_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "response_metadata": { + "name": "response_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_delivery_org_idempotency_unique": { + "name": "integration_delivery_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_deliveries_organisation_id_organisations_id_fk": { + "name": "integration_deliveries_organisation_id_organisations_id_fk", + "tableFrom": "integration_deliveries", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_deliveries_integration_id_integration_records_id_fk": { + "name": "integration_deliveries_integration_id_integration_records_id_fk", + "tableFrom": "integration_deliveries", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_entities": { + "name": "integration_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "posture": { + "name": "posture", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "integration_entities_org_external_unique": { + "name": "integration_entities_org_external_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_entities_organisation_id_organisations_id_fk": { + "name": "integration_entities_organisation_id_organisations_id_fk", + "tableFrom": "integration_entities", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_entities_integration_id_integration_records_id_fk": { + "name": "integration_entities_integration_id_integration_records_id_fk", + "tableFrom": "integration_entities", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_query_runs": { + "name": "integration_query_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "request_metadata": { + "name": "request_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "response_metadata": { + "name": "response_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_query_runs_org_idempotency_unique": { + "name": "integration_query_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_query_runs_org_status_idx": { + "name": "integration_query_runs_org_status_idx", + "columns": [ + { + "expression": "organisation_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": { + "integration_query_runs_organisation_id_organisations_id_fk": { + "name": "integration_query_runs_organisation_id_organisations_id_fk", + "tableFrom": "integration_query_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_runs_integration_id_integration_records_id_fk": { + "name": "integration_query_runs_integration_id_integration_records_id_fk", + "tableFrom": "integration_query_runs", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_runs_template_id_integration_query_templates_id_fk": { + "name": "integration_query_runs_template_id_integration_query_templates_id_fk", + "tableFrom": "integration_query_runs", + "tableTo": "integration_query_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_runs_requested_by_actor_id_actors_id_fk": { + "name": "integration_query_runs_requested_by_actor_id_actors_id_fk", + "tableFrom": "integration_query_runs", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_query_templates": { + "name": "integration_query_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_key": { + "name": "template_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "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": { + "integration_templates_org_key_version_unique": { + "name": "integration_templates_org_key_version_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_query_templates_organisation_id_organisations_id_fk": { + "name": "integration_query_templates_organisation_id_organisations_id_fk", + "tableFrom": "integration_query_templates", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_templates_integration_id_integration_records_id_fk": { + "name": "integration_query_templates_integration_id_integration_records_id_fk", + "tableFrom": "integration_query_templates", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "integration_query_templates_created_by_actor_id_actors_id_fk": { + "name": "integration_query_templates_created_by_actor_id_actors_id_fk", + "tableFrom": "integration_query_templates", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_records": { + "name": "integration_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mock": { + "name": "mock", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "configuration": { + "name": "configuration", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "health": { + "name": "health", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cursor": { + "name": "cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integrations_org_product_instance_unique": { + "name": "integrations_org_product_instance_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_records_organisation_id_organisations_id_fk": { + "name": "integration_records_organisation_id_organisations_id_fk", + "tableFrom": "integration_records", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.investigations": { + "name": "investigations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "investigation_number": { + "name": "investigation_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "investigation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "severity": { + "name": "severity", + "type": "severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "lead_actor_id": { + "name": "lead_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recommendation": { + "name": "recommendation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promotion_decision": { + "name": "promotion_decision", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "linked_kelpie_case_id": { + "name": "linked_kelpie_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "investigations_org_number_unique": { + "name": "investigations_org_number_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "investigation_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "investigations_org_queue_idx": { + "name": "investigations_org_queue_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_activity_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "investigations_search_idx": { + "name": "investigations_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"title\" || ' ' || \"summary\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "investigations_organisation_id_organisations_id_fk": { + "name": "investigations_organisation_id_organisations_id_fk", + "tableFrom": "investigations", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "investigations_lead_actor_id_actors_id_fk": { + "name": "investigations_lead_actor_id_actors_id_fk", + "tableFrom": "investigations", + "tableTo": "actors", + "columnsFrom": [ + "lead_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "investigations_room_id_rooms_id_fk": { + "name": "investigations_room_id_rooms_id_fk", + "tableFrom": "investigations", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_mentions": { + "name": "message_mentions", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mentioned_actor_id": { + "name": "mentioned_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mention_type": { + "name": "mention_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mention_key": { + "name": "mention_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_mentions_org_actor_idx": { + "name": "message_mentions_org_actor_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mentioned_actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_mentions_organisation_id_organisations_id_fk": { + "name": "message_mentions_organisation_id_organisations_id_fk", + "tableFrom": "message_mentions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_mentions_message_id_messages_id_fk": { + "name": "message_mentions_message_id_messages_id_fk", + "tableFrom": "message_mentions", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_mentions_mentioned_actor_id_actors_id_fk": { + "name": "message_mentions_mentioned_actor_id_actors_id_fk", + "tableFrom": "message_mentions", + "tableTo": "actors", + "columnsFrom": [ + "mentioned_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_mentions_message_id_mention_type_mention_key_pk": { + "name": "message_mentions_message_id_mention_type_mention_key_pk", + "columns": [ + "message_id", + "mention_type", + "mention_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "message_mention_type_check": { + "name": "message_mention_type_check", + "value": "\"message_mentions\".\"mention_type\" in ('actor','room','everyone')" + } + }, + "isRLSEnabled": false + }, + "public.message_pins": { + "name": "message_pins", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pinned_by_actor_id": { + "name": "pinned_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_pins_org_room_idx": { + "name": "message_pins_org_room_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_pins_organisation_id_organisations_id_fk": { + "name": "message_pins_organisation_id_organisations_id_fk", + "tableFrom": "message_pins", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_pins_room_id_rooms_id_fk": { + "name": "message_pins_room_id_rooms_id_fk", + "tableFrom": "message_pins", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_pins_message_id_messages_id_fk": { + "name": "message_pins_message_id_messages_id_fk", + "tableFrom": "message_pins", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_pins_pinned_by_actor_id_actors_id_fk": { + "name": "message_pins_pinned_by_actor_id_actors_id_fk", + "tableFrom": "message_pins", + "tableTo": "actors", + "columnsFrom": [ + "pinned_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_pins_room_id_message_id_pk": { + "name": "message_pins_room_id_message_id_pk", + "columns": [ + "room_id", + "message_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.message_revisions": { + "name": "message_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_type": { + "name": "revision_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previous_document": { + "name": "previous_document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "previous_plain_text": { + "name": "previous_plain_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "next_document": { + "name": "next_document", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_plain_text": { + "name": "next_plain_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_revisions_org_message_idx": { + "name": "message_revisions_org_message_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "message_revisions_org_idempotency_unique": { + "name": "message_revisions_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_revisions_organisation_id_organisations_id_fk": { + "name": "message_revisions_organisation_id_organisations_id_fk", + "tableFrom": "message_revisions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_revisions_message_id_messages_id_fk": { + "name": "message_revisions_message_id_messages_id_fk", + "tableFrom": "message_revisions", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_revisions_actor_id_actors_id_fk": { + "name": "message_revisions_actor_id_actors_id_fk", + "tableFrom": "message_revisions", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "message_revision_type_check": { + "name": "message_revision_type_check", + "value": "\"message_revisions\".\"revision_type\" in ('edit','delete')" + } + }, + "isRLSEnabled": false + }, + "public.message_saves": { + "name": "message_saves", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "message_saves_org_actor_idx": { + "name": "message_saves_org_actor_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "message_saves_organisation_id_organisations_id_fk": { + "name": "message_saves_organisation_id_organisations_id_fk", + "tableFrom": "message_saves", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_saves_message_id_messages_id_fk": { + "name": "message_saves_message_id_messages_id_fk", + "tableFrom": "message_saves", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_saves_actor_id_actors_id_fk": { + "name": "message_saves_actor_id_actors_id_fk", + "tableFrom": "message_saves", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "message_saves_message_id_actor_id_pk": { + "name": "message_saves_message_id_actor_id_pk", + "columns": [ + "message_id", + "actor_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "thread_parent_id": { + "name": "thread_parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_actor_id": { + "name": "author_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_type": { + "name": "message_type", + "type": "message_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "plain_text": { + "name": "plain_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "edited_at": { + "name": "edited_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "data_classification": { + "name": "data_classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "related_alert_id": { + "name": "related_alert_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_investigation_id": { + "name": "related_investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_case_id": { + "name": "related_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_agent_run_id": { + "name": "related_agent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_workflow_run_id": { + "name": "related_workflow_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "messages_org_room_time_idx": { + "name": "messages_org_room_time_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_thread_idx": { + "name": "messages_thread_idx", + "columns": [ + { + "expression": "thread_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_org_idempotency_unique": { + "name": "messages_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"messages\".\"idempotency_key\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "messages_search_idx": { + "name": "messages_search_idx", + "columns": [ + { + "expression": "to_tsvector('english', \"plain_text\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "messages_organisation_id_organisations_id_fk": { + "name": "messages_organisation_id_organisations_id_fk", + "tableFrom": "messages", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "messages_room_id_rooms_id_fk": { + "name": "messages_room_id_rooms_id_fk", + "tableFrom": "messages", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "messages_author_actor_id_actors_id_fk": { + "name": "messages_author_actor_id_actors_id_fk", + "tableFrom": "messages", + "tableTo": "actors", + "columnsFrom": [ + "author_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "messages_related_alert_id_alerts_id_fk": { + "name": "messages_related_alert_id_alerts_id_fk", + "tableFrom": "messages", + "tableTo": "alerts", + "columnsFrom": [ + "related_alert_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "messages_related_investigation_id_investigations_id_fk": { + "name": "messages_related_investigation_id_investigations_id_fk", + "tableFrom": "messages", + "tableTo": "investigations", + "columnsFrom": [ + "related_investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "safe_preview": { + "name": "safe_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_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": { + "notifications_org_actor_read_idx": { + "name": "notifications_org_actor_read_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "read_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_organisation_id_organisations_id_fk": { + "name": "notifications_organisation_id_organisations_id_fk", + "tableFrom": "notifications", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notifications_actor_id_actors_id_fk": { + "name": "notifications_actor_id_actors_id_fk", + "tableFrom": "notifications", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organisations": { + "name": "organisations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data_region": { + "name": "data_region", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'australia'" + }, + "default_timezone": { + "name": "default_timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Australia/Sydney'" + }, + "retention_policy": { + "name": "retention_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "authentication_policy": { + "name": "authentication_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organisations_slug_unique": { + "name": "organisations_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organisations_status_check": { + "name": "organisations_status_check", + "value": "\"organisations\".\"status\" in ('active','suspended')" + } + }, + "isRLSEnabled": false + }, + "public.outbox_events": { + "name": "outbox_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aggregate_type": { + "name": "aggregate_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "aggregate_id": { + "name": "aggregate_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queue_name": { + "name": "queue_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "outbox_idempotency_unique": { + "name": "outbox_idempotency_unique", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_pending_idx": { + "name": "outbox_pending_idx", + "columns": [ + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"outbox_events\".\"dispatched_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "outbox_events_organisation_id_organisations_id_fk": { + "name": "outbox_events_organisation_id_organisations_id_fk", + "tableFrom": "outbox_events", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reaction_operations": { + "name": "reaction_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "result_count": { + "name": "result_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reaction_operations_org_idempotency_unique": { + "name": "reaction_operations_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_operations_org_message_idx": { + "name": "reaction_operations_org_message_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_operations_organisation_id_organisations_id_fk": { + "name": "reaction_operations_organisation_id_organisations_id_fk", + "tableFrom": "reaction_operations", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_operations_message_id_messages_id_fk": { + "name": "reaction_operations_message_id_messages_id_fk", + "tableFrom": "reaction_operations", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_operations_actor_id_actors_id_fk": { + "name": "reaction_operations_actor_id_actors_id_fk", + "tableFrom": "reaction_operations", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reaction_pack_assets": { + "name": "reaction_pack_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_id": { + "name": "revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "frame_count": { + "name": "frame_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_state": { + "name": "verification_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'verified'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reaction_pack_assets_org_revision_name_unique": { + "name": "reaction_pack_assets_org_revision_name_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_pack_assets_org_digest_idx": { + "name": "reaction_pack_assets_org_digest_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_pack_assets_organisation_id_organisations_id_fk": { + "name": "reaction_pack_assets_organisation_id_organisations_id_fk", + "tableFrom": "reaction_pack_assets", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_pack_assets_revision_id_reaction_pack_revisions_id_fk": { + "name": "reaction_pack_assets_revision_id_reaction_pack_revisions_id_fk", + "tableFrom": "reaction_pack_assets", + "tableTo": "reaction_pack_revisions", + "columnsFrom": [ + "revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reaction_pack_assets_verification_check": { + "name": "reaction_pack_assets_verification_check", + "value": "\"reaction_pack_assets\".\"verification_state\" in ('verified','missing','mismatch')" + }, + "reaction_pack_assets_dimensions_check": { + "name": "reaction_pack_assets_dimensions_check", + "value": "\"reaction_pack_assets\".\"width\" > 0 and \"reaction_pack_assets\".\"height\" > 0 and \"reaction_pack_assets\".\"frame_count\" > 0 and \"reaction_pack_assets\".\"byte_size\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.reaction_pack_revisions": { + "name": "reaction_pack_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_actor_id": { + "name": "approved_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "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": { + "reaction_pack_revisions_org_pack_revision_unique": { + "name": "reaction_pack_revisions_org_pack_revision_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pack_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_pack_revisions_org_status_idx": { + "name": "reaction_pack_revisions_org_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_pack_revisions_organisation_id_organisations_id_fk": { + "name": "reaction_pack_revisions_organisation_id_organisations_id_fk", + "tableFrom": "reaction_pack_revisions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_pack_revisions_pack_id_reaction_packs_id_fk": { + "name": "reaction_pack_revisions_pack_id_reaction_packs_id_fk", + "tableFrom": "reaction_pack_revisions", + "tableTo": "reaction_packs", + "columnsFrom": [ + "pack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_pack_revisions_approved_by_actor_id_actors_id_fk": { + "name": "reaction_pack_revisions_approved_by_actor_id_actors_id_fk", + "tableFrom": "reaction_pack_revisions", + "tableTo": "actors", + "columnsFrom": [ + "approved_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_pack_revisions_created_by_actor_id_actors_id_fk": { + "name": "reaction_pack_revisions_created_by_actor_id_actors_id_fk", + "tableFrom": "reaction_pack_revisions", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reaction_pack_revisions_status_check": { + "name": "reaction_pack_revisions_status_check", + "value": "\"reaction_pack_revisions\".\"status\" in ('draft','approved','superseded','removed')" + }, + "reaction_pack_revisions_revision_check": { + "name": "reaction_pack_revisions_revision_check", + "value": "\"reaction_pack_revisions\".\"revision\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.reaction_packs": { + "name": "reaction_packs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "removed_by_actor_id": { + "name": "removed_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reaction_packs_org_slug_unique": { + "name": "reaction_packs_org_slug_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_packs_org_lifecycle_idx": { + "name": "reaction_packs_org_lifecycle_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lifecycle", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_packs_organisation_id_organisations_id_fk": { + "name": "reaction_packs_organisation_id_organisations_id_fk", + "tableFrom": "reaction_packs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_packs_created_by_actor_id_actors_id_fk": { + "name": "reaction_packs_created_by_actor_id_actors_id_fk", + "tableFrom": "reaction_packs", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reaction_packs_removed_by_actor_id_actors_id_fk": { + "name": "reaction_packs_removed_by_actor_id_actors_id_fk", + "tableFrom": "reaction_packs", + "tableTo": "actors", + "columnsFrom": [ + "removed_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reaction_packs_lifecycle_check": { + "name": "reaction_packs_lifecycle_check", + "value": "\"reaction_packs\".\"lifecycle\" in ('active','removed')" + } + }, + "isRLSEnabled": false + }, + "public.reactions": { + "name": "reactions", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reactions_organisation_id_organisations_id_fk": { + "name": "reactions_organisation_id_organisations_id_fk", + "tableFrom": "reactions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reactions_message_id_messages_id_fk": { + "name": "reactions_message_id_messages_id_fk", + "tableFrom": "reactions", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reactions_actor_id_actors_id_fk": { + "name": "reactions_actor_id_actors_id_fk", + "tableFrom": "reactions", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "reactions_message_id_actor_id_emoji_pk": { + "name": "reactions_message_id_actor_id_emoji_pk", + "columns": [ + "message_id", + "actor_id", + "emoji" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.report_deliveries": { + "name": "report_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "report_id": { + "name": "report_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient": { + "name": "recipient", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'awaiting_approval'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "report_deliveries_org_idempotency_unique": { + "name": "report_deliveries_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "report_deliveries_org_report_status_idx": { + "name": "report_deliveries_org_report_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "report_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "report_deliveries_organisation_id_organisations_id_fk": { + "name": "report_deliveries_organisation_id_organisations_id_fk", + "tableFrom": "report_deliveries", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_deliveries_report_id_report_manifests_id_fk": { + "name": "report_deliveries_report_id_report_manifests_id_fk", + "tableFrom": "report_deliveries", + "tableTo": "report_manifests", + "columnsFrom": [ + "report_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_deliveries_approval_id_approvals_id_fk": { + "name": "report_deliveries_approval_id_approvals_id_fk", + "tableFrom": "report_deliveries", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_deliveries_requested_by_actor_id_actors_id_fk": { + "name": "report_deliveries_requested_by_actor_id_actors_id_fk", + "tableFrom": "report_deliveries", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "report_deliveries_status_check": { + "name": "report_deliveries_status_check", + "value": "\"report_deliveries\".\"status\" in ('awaiting_approval','queued','delivered','failed','cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.report_manifests": { + "name": "report_manifests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "classification": { + "name": "classification", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "review_note": { + "name": "review_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "posted_message_id": { + "name": "posted_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "report_manifests_org_idempotency_unique": { + "name": "report_manifests_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "report_manifests_org_room_status_idx": { + "name": "report_manifests_org_room_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_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": { + "report_manifests_organisation_id_organisations_id_fk": { + "name": "report_manifests_organisation_id_organisations_id_fk", + "tableFrom": "report_manifests", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_agent_run_id_agent_runs_id_fk": { + "name": "report_manifests_agent_run_id_agent_runs_id_fk", + "tableFrom": "report_manifests", + "tableTo": "agent_runs", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_task_id_tasks_id_fk": { + "name": "report_manifests_task_id_tasks_id_fk", + "tableFrom": "report_manifests", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_room_id_rooms_id_fk": { + "name": "report_manifests_room_id_rooms_id_fk", + "tableFrom": "report_manifests", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_requested_by_actor_id_actors_id_fk": { + "name": "report_manifests_requested_by_actor_id_actors_id_fk", + "tableFrom": "report_manifests", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_manifests_posted_message_id_messages_id_fk": { + "name": "report_manifests_posted_message_id_messages_id_fk", + "tableFrom": "report_manifests", + "tableTo": "messages", + "columnsFrom": [ + "posted_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "report_manifests_status_check": { + "name": "report_manifests_status_check", + "value": "\"report_manifests\".\"status\" in ('draft','reviewed','posted','superseded')" + }, + "report_manifests_version_check": { + "name": "report_manifests_version_check", + "value": "\"report_manifests\".\"version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.report_schedules": { + "name": "report_schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'leadership'" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "report_schedules_org_idempotency_unique": { + "name": "report_schedules_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "report_schedules_org_due_idx": { + "name": "report_schedules_org_due_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "report_schedules_organisation_id_organisations_id_fk": { + "name": "report_schedules_organisation_id_organisations_id_fk", + "tableFrom": "report_schedules", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_schedules_room_id_rooms_id_fk": { + "name": "report_schedules_room_id_rooms_id_fk", + "tableFrom": "report_schedules", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "report_schedules_created_by_actor_id_actors_id_fk": { + "name": "report_schedules_created_by_actor_id_actors_id_fk", + "tableFrom": "report_schedules", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "report_schedules_cadence_check": { + "name": "report_schedules_cadence_check", + "value": "\"report_schedules\".\"cadence\" in ('weekly','monthly')" + }, + "report_schedules_audience_check": { + "name": "report_schedules_audience_check", + "value": "\"report_schedules\".\"audience\" in ('analyst','leadership','executive')" + } + }, + "isRLSEnabled": false + }, + "public.research_items": { + "name": "research_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "watchlist_id": { + "name": "watchlist_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "research_run_id": { + "name": "research_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_published_at": { + "name": "source_published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "root_message_id": { + "name": "root_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_message_id": { + "name": "latest_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "brief": { + "name": "brief", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_by_actor_id": { + "name": "feedback_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "feedback_at": { + "name": "feedback_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "research_items_org_fingerprint_unique": { + "name": "research_items_org_fingerprint_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_items_org_watchlist_idx": { + "name": "research_items_org_watchlist_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "watchlist_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "research_items_organisation_id_organisations_id_fk": { + "name": "research_items_organisation_id_organisations_id_fk", + "tableFrom": "research_items", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_watchlist_id_research_watchlists_id_fk": { + "name": "research_items_watchlist_id_research_watchlists_id_fk", + "tableFrom": "research_items", + "tableTo": "research_watchlists", + "columnsFrom": [ + "watchlist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_research_run_id_research_runs_id_fk": { + "name": "research_items_research_run_id_research_runs_id_fk", + "tableFrom": "research_items", + "tableTo": "research_runs", + "columnsFrom": [ + "research_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_root_message_id_messages_id_fk": { + "name": "research_items_root_message_id_messages_id_fk", + "tableFrom": "research_items", + "tableTo": "messages", + "columnsFrom": [ + "root_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_latest_message_id_messages_id_fk": { + "name": "research_items_latest_message_id_messages_id_fk", + "tableFrom": "research_items", + "tableTo": "messages", + "columnsFrom": [ + "latest_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_items_feedback_by_actor_id_actors_id_fk": { + "name": "research_items_feedback_by_actor_id_actors_id_fk", + "tableFrom": "research_items", + "tableTo": "actors", + "columnsFrom": [ + "feedback_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.research_runs": { + "name": "research_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "watchlist_id": { + "name": "watchlist_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "source_limit": { + "name": "source_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_budget": { + "name": "token_budget", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost_limit_cents": { + "name": "cost_limit_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "time_limit_seconds": { + "name": "time_limit_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "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": { + "research_runs_org_idempotency_unique": { + "name": "research_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_runs_org_agent_unique": { + "name": "research_runs_org_agent_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_runs_org_status_idx": { + "name": "research_runs_org_status_idx", + "columns": [ + { + "expression": "organisation_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": { + "research_runs_organisation_id_organisations_id_fk": { + "name": "research_runs_organisation_id_organisations_id_fk", + "tableFrom": "research_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_runs_watchlist_id_research_watchlists_id_fk": { + "name": "research_runs_watchlist_id_research_watchlists_id_fk", + "tableFrom": "research_runs", + "tableTo": "research_watchlists", + "columnsFrom": [ + "watchlist_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_runs_agent_run_id_agent_runs_id_fk": { + "name": "research_runs_agent_run_id_agent_runs_id_fk", + "tableFrom": "research_runs", + "tableTo": "agent_runs", + "columnsFrom": [ + "agent_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "research_runs_status_check": { + "name": "research_runs_status_check", + "value": "\"research_runs\".\"status\" in ('queued','running','completed','failed')" + } + }, + "isRLSEnabled": false + }, + "public.research_watchlists": { + "name": "research_watchlists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vendors": { + "name": "vendors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "technologies": { + "name": "technologies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cadence_minutes": { + "name": "cadence_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 240 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "research_watchlists_org_name_unique": { + "name": "research_watchlists_org_name_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "research_watchlists_due_idx": { + "name": "research_watchlists_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "research_watchlists_organisation_id_organisations_id_fk": { + "name": "research_watchlists_organisation_id_organisations_id_fk", + "tableFrom": "research_watchlists", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_watchlists_room_id_rooms_id_fk": { + "name": "research_watchlists_room_id_rooms_id_fk", + "tableFrom": "research_watchlists", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "research_watchlists_created_by_actor_id_actors_id_fk": { + "name": "research_watchlists_created_by_actor_id_actors_id_fk", + "tableFrom": "research_watchlists", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "research_watchlists_cadence_check": { + "name": "research_watchlists_cadence_check", + "value": "\"research_watchlists\".\"cadence_minutes\" between 15 and 10080" + } + }, + "isRLSEnabled": false + }, + "public.room_integration_bindings": { + "name": "room_integration_bindings", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "integration_id": { + "name": "integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "room_integration_bindings_org_room_idx": { + "name": "room_integration_bindings_org_room_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "room_integration_bindings_organisation_id_organisations_id_fk": { + "name": "room_integration_bindings_organisation_id_organisations_id_fk", + "tableFrom": "room_integration_bindings", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_integration_bindings_room_id_rooms_id_fk": { + "name": "room_integration_bindings_room_id_rooms_id_fk", + "tableFrom": "room_integration_bindings", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_integration_bindings_integration_id_integration_records_id_fk": { + "name": "room_integration_bindings_integration_id_integration_records_id_fk", + "tableFrom": "room_integration_bindings", + "tableTo": "integration_records", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_integration_bindings_created_by_actor_id_actors_id_fk": { + "name": "room_integration_bindings_created_by_actor_id_actors_id_fk", + "tableFrom": "room_integration_bindings", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "room_integration_bindings_room_id_integration_id_pk": { + "name": "room_integration_bindings_room_id_integration_id_pk", + "columns": [ + "room_id", + "integration_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.room_invitations": { + "name": "room_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invited_actor_id": { + "name": "invited_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_expires_at": { + "name": "access_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by_actor_id": { + "name": "invited_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "responded_at": { + "name": "responded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "room_invitations_org_idempotency_unique": { + "name": "room_invitations_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "room_invitations_org_room_status_idx": { + "name": "room_invitations_org_room_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "room_invitations_organisation_id_organisations_id_fk": { + "name": "room_invitations_organisation_id_organisations_id_fk", + "tableFrom": "room_invitations", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_invitations_room_id_rooms_id_fk": { + "name": "room_invitations_room_id_rooms_id_fk", + "tableFrom": "room_invitations", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_invitations_invited_actor_id_actors_id_fk": { + "name": "room_invitations_invited_actor_id_actors_id_fk", + "tableFrom": "room_invitations", + "tableTo": "actors", + "columnsFrom": [ + "invited_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_invitations_invited_by_actor_id_actors_id_fk": { + "name": "room_invitations_invited_by_actor_id_actors_id_fk", + "tableFrom": "room_invitations", + "tableTo": "actors", + "columnsFrom": [ + "invited_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "room_invitation_role_check": { + "name": "room_invitation_role_check", + "value": "\"room_invitations\".\"membership_role\" in ('moderator','member','guest','agent_member')" + }, + "room_invitation_status_check": { + "name": "room_invitation_status_check", + "value": "\"room_invitations\".\"status\" in ('pending','accepted','revoked','expired')" + } + }, + "isRLSEnabled": false + }, + "public.room_memberships": { + "name": "room_memberships", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_level": { + "name": "notification_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "notify_replies": { + "name": "notify_replies", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_followed_threads": { + "name": "notify_followed_threads", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_read_event_id": { + "name": "last_read_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "muted": { + "name": "muted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "favourite": { + "name": "favourite", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sidebar_position": { + "name": "sidebar_position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sidebar_group": { + "name": "sidebar_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_expires_at": { + "name": "access_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "room_memberships_org_actor_idx": { + "name": "room_memberships_org_actor_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "room_memberships_organisation_id_organisations_id_fk": { + "name": "room_memberships_organisation_id_organisations_id_fk", + "tableFrom": "room_memberships", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_memberships_room_id_rooms_id_fk": { + "name": "room_memberships_room_id_rooms_id_fk", + "tableFrom": "room_memberships", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "room_memberships_actor_id_actors_id_fk": { + "name": "room_memberships_actor_id_actors_id_fk", + "tableFrom": "room_memberships", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "room_memberships_room_id_actor_id_pk": { + "name": "room_memberships_room_id_actor_id_pk", + "columns": [ + "room_id", + "actor_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "room_membership_role_check": { + "name": "room_membership_role_check", + "value": "\"room_memberships\".\"membership_role\" in ('owner','moderator','member','guest','agent_member')" + } + }, + "isRLSEnabled": false + }, + "public.rooms": { + "name": "rooms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "room_type": { + "name": "room_type", + "type": "room_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'organisation'" + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "policies": { + "name": "policies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "direct_fingerprint": { + "name": "direct_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "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()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "linked_investigation_id": { + "name": "linked_investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_kelpie_case_id": { + "name": "linked_kelpie_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_severity": { + "name": "default_severity", + "type": "severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "tlp": { + "name": "tlp", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'amber'" + }, + "retention_policy": { + "name": "retention_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "rooms_org_slug_unique": { + "name": "rooms_org_slug_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rooms_org_direct_fingerprint_unique": { + "name": "rooms_org_direct_fingerprint_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direct_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"rooms\".\"direct_fingerprint\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "rooms_org_type_idx": { + "name": "rooms_org_type_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "room_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rooms_organisation_id_organisations_id_fk": { + "name": "rooms_organisation_id_organisations_id_fk", + "tableFrom": "rooms", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "rooms_created_by_actor_id_actors_id_fk": { + "name": "rooms_created_by_actor_id_actors_id_fk", + "tableFrom": "rooms", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.synthetic_artifact_provenance": { + "name": "synthetic_artifact_provenance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "artifact_table": { + "name": "artifact_table", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifact_id": { + "name": "artifact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_reference": { + "name": "source_reference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recorded_by_actor_id": { + "name": "recorded_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "synthetic_artifact_provenance_artifact_unique": { + "name": "synthetic_artifact_provenance_artifact_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "artifact_table", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "artifact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "synthetic_artifact_provenance_organisation_id_organisations_id_fk": { + "name": "synthetic_artifact_provenance_organisation_id_organisations_id_fk", + "tableFrom": "synthetic_artifact_provenance", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "synthetic_artifact_provenance_recorded_by_actor_id_actors_id_fk": { + "name": "synthetic_artifact_provenance_recorded_by_actor_id_actors_id_fk", + "tableFrom": "synthetic_artifact_provenance", + "tableTo": "actors", + "columnsFrom": [ + "recorded_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "synthetic_artifact_provenance_table_check": { + "name": "synthetic_artifact_provenance_table_check", + "value": "\"synthetic_artifact_provenance\".\"artifact_table\" in ('rooms','tasks','hunts','integrations','researchWatchlists','reportManifests','reportSchedules','messages','evidence','agentMemories','actors')" + }, + "synthetic_artifact_provenance_source_check": { + "name": "synthetic_artifact_provenance_source_check", + "value": "\"synthetic_artifact_provenance\".\"source_kind\" in ('seed_fixture','mock_runtime','test_fixture','legacy_live_proof')" + } + }, + "isRLSEnabled": false + }, + "public.synthetic_cleanup_object_deletion_attempts": { + "name": "synthetic_cleanup_object_deletion_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "manifest_id": { + "name": "manifest_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "evidence_id": { + "name": "evidence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorization_approval_id": { + "name": "authorization_approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempted_by_actor_id": { + "name": "attempted_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "synthetic_cleanup_object_attempts_manifest_idx": { + "name": "synthetic_cleanup_object_attempts_manifest_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "manifest_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "synthetic_cleanup_object_deletion_attempts_manifest_id_synthetic_cleanup_receipts_manifest_id_fk": { + "name": "synthetic_cleanup_object_deletion_attempts_manifest_id_synthetic_cleanup_receipts_manifest_id_fk", + "tableFrom": "synthetic_cleanup_object_deletion_attempts", + "tableTo": "synthetic_cleanup_receipts", + "columnsFrom": [ + "manifest_id" + ], + "columnsTo": [ + "manifest_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "synthetic_cleanup_object_deletion_attempts_organisation_id_organisations_id_fk": { + "name": "synthetic_cleanup_object_deletion_attempts_organisation_id_organisations_id_fk", + "tableFrom": "synthetic_cleanup_object_deletion_attempts", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "synthetic_cleanup_object_deletion_attempts_authorization_approval_id_approvals_id_fk": { + "name": "synthetic_cleanup_object_deletion_attempts_authorization_approval_id_approvals_id_fk", + "tableFrom": "synthetic_cleanup_object_deletion_attempts", + "tableTo": "approvals", + "columnsFrom": [ + "authorization_approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "synthetic_cleanup_object_deletion_attempts_attempted_by_actor_id_actors_id_fk": { + "name": "synthetic_cleanup_object_deletion_attempts_attempted_by_actor_id_actors_id_fk", + "tableFrom": "synthetic_cleanup_object_deletion_attempts", + "tableTo": "actors", + "columnsFrom": [ + "attempted_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "synthetic_cleanup_object_attempts_result_check": { + "name": "synthetic_cleanup_object_attempts_result_check", + "value": "\"synthetic_cleanup_object_deletion_attempts\".\"result\" in ('started','succeeded','failed','observed_missing')" + } + }, + "isRLSEnabled": false + }, + "public.synthetic_cleanup_receipts": { + "name": "synthetic_cleanup_receipts", + "schema": "", + "columns": { + "manifest_id": { + "name": "manifest_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "maintenance_actor_id": { + "name": "maintenance_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "manifest_digest": { + "name": "manifest_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "candidate_counts": { + "name": "candidate_counts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pre_digests": { + "name": "pre_digests", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "post_digests": { + "name": "post_digests", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "object_storage_objects": { + "name": "object_storage_objects", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "synthetic_cleanup_receipts_approval_unique": { + "name": "synthetic_cleanup_receipts_approval_unique", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "synthetic_cleanup_receipts_org_digest_unique": { + "name": "synthetic_cleanup_receipts_org_digest_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "manifest_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "synthetic_cleanup_receipts_organisation_id_organisations_id_fk": { + "name": "synthetic_cleanup_receipts_organisation_id_organisations_id_fk", + "tableFrom": "synthetic_cleanup_receipts", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "synthetic_cleanup_receipts_approval_id_approvals_id_fk": { + "name": "synthetic_cleanup_receipts_approval_id_approvals_id_fk", + "tableFrom": "synthetic_cleanup_receipts", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "synthetic_cleanup_receipts_maintenance_actor_id_actors_id_fk": { + "name": "synthetic_cleanup_receipts_maintenance_actor_id_actors_id_fk", + "tableFrom": "synthetic_cleanup_receipts", + "tableTo": "actors", + "columnsFrom": [ + "maintenance_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "priority": { + "name": "priority", + "type": "task_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "assigned_actor_id": { + "name": "assigned_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_case_id": { + "name": "related_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approval_required": { + "name": "approval_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "agent_run_id": { + "name": "agent_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_run_status": { + "name": "agent_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_org_status_idx": { + "name": "tasks_org_status_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_org_assignee_idx": { + "name": "tasks_org_assignee_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assigned_actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_org_idempotency_unique": { + "name": "tasks_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_organisation_id_organisations_id_fk": { + "name": "tasks_organisation_id_organisations_id_fk", + "tableFrom": "tasks", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_assigned_actor_id_actors_id_fk": { + "name": "tasks_assigned_actor_id_actors_id_fk", + "tableFrom": "tasks", + "tableTo": "actors", + "columnsFrom": [ + "assigned_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_created_by_actor_id_actors_id_fk": { + "name": "tasks_created_by_actor_id_actors_id_fk", + "tableFrom": "tasks", + "tableTo": "actors", + "columnsFrom": [ + "created_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_room_id_rooms_id_fk": { + "name": "tasks_room_id_rooms_id_fk", + "tableFrom": "tasks", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_investigation_id_investigations_id_fk": { + "name": "tasks_investigation_id_investigations_id_fk", + "tableFrom": "tasks", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.thread_follows": { + "name": "thread_follows", + "schema": "", + "columns": { + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "root_message_id": { + "name": "root_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "thread_follows_org_actor_idx": { + "name": "thread_follows_org_actor_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "thread_follows_organisation_id_organisations_id_fk": { + "name": "thread_follows_organisation_id_organisations_id_fk", + "tableFrom": "thread_follows", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "thread_follows_root_message_id_messages_id_fk": { + "name": "thread_follows_root_message_id_messages_id_fk", + "tableFrom": "thread_follows", + "tableTo": "messages", + "columnsFrom": [ + "root_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "thread_follows_actor_id_actors_id_fk": { + "name": "thread_follows_actor_id_actors_id_fk", + "tableFrom": "thread_follows", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "thread_follows_root_message_id_actor_id_pk": { + "name": "thread_follows_root_message_id_actor_id_pk", + "columns": [ + "root_message_id", + "actor_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.timeline_events": { + "name": "timeline_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "external_case_id": { + "name": "external_case_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "occurred_at": { + "name": "occurred_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()" + } + }, + "indexes": { + "timeline_org_investigation_time_idx": { + "name": "timeline_org_investigation_time_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "investigation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "timeline_events_organisation_id_organisations_id_fk": { + "name": "timeline_events_organisation_id_organisations_id_fk", + "tableFrom": "timeline_events", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "timeline_events_investigation_id_investigations_id_fk": { + "name": "timeline_events_investigation_id_investigations_id_fk", + "tableFrom": "timeline_events", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "timeline_events_room_id_rooms_id_fk": { + "name": "timeline_events_room_id_rooms_id_fk", + "tableFrom": "timeline_events", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "timeline_events_actor_id_actors_id_fk": { + "name": "timeline_events_actor_id_actors_id_fk", + "tableFrom": "timeline_events", + "tableTo": "actors", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "better_auth_user_id": { + "name": "better_auth_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team": { + "name": "team", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "presence_state": { + "name": "presence_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'offline'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Australia/Sydney'" + }, + "notification_preferences": { + "name": "notification_preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_org_email_unique": { + "name": "users_org_email_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_better_auth_unique": { + "name": "users_better_auth_unique", + "columns": [ + { + "expression": "better_auth_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_org_idx": { + "name": "users_org_idx", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_organisation_id_organisations_id_fk": { + "name": "users_organisation_id_organisations_id_fk", + "tableFrom": "users", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_better_auth_user_id_auth_user_id_fk": { + "name": "users_better_auth_user_id_auth_user_id_fk", + "tableFrom": "users", + "tableTo": "auth_user", + "columnsFrom": [ + "better_auth_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_definitions": { + "name": "workflow_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_key": { + "name": "workflow_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "yaml": { + "name": "yaml", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parsed": { + "name": "parsed", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "owner_actor_id": { + "name": "owner_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "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": { + "workflow_defs_org_key_version_unique": { + "name": "workflow_defs_org_key_version_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_definitions_organisation_id_organisations_id_fk": { + "name": "workflow_definitions_organisation_id_organisations_id_fk", + "tableFrom": "workflow_definitions", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_definitions_owner_actor_id_actors_id_fk": { + "name": "workflow_definitions_owner_actor_id_actors_id_fk", + "tableFrom": "workflow_definitions", + "tableTo": "actors", + "columnsFrom": [ + "owner_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_runs": { + "name": "workflow_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_definition_id": { + "name": "workflow_definition_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "trigger_event_id": { + "name": "trigger_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "investigation_id": { + "name": "investigation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workflow_runs_org_idempotency_unique": { + "name": "workflow_runs_org_idempotency_unique", + "columns": [ + { + "expression": "organisation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_runs_organisation_id_organisations_id_fk": { + "name": "workflow_runs_organisation_id_organisations_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "organisations", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_runs_workflow_definition_id_workflow_definitions_id_fk": { + "name": "workflow_runs_workflow_definition_id_workflow_definitions_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "workflow_definitions", + "columnsFrom": [ + "workflow_definition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_runs_room_id_rooms_id_fk": { + "name": "workflow_runs_room_id_rooms_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_runs_investigation_id_investigations_id_fk": { + "name": "workflow_runs_investigation_id_investigations_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "investigations", + "columnsFrom": [ + "investigation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_runs_requested_by_actor_id_actors_id_fk": { + "name": "workflow_runs_requested_by_actor_id_actors_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "actors", + "columnsFrom": [ + "requested_by_actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.actor_type": { + "name": "actor_type", + "schema": "public", + "values": [ + "human", + "agent", + "product", + "service", + "system" + ] + }, + "public.alert_status": { + "name": "alert_status", + "schema": "public", + "values": [ + "new", + "acknowledged", + "investigating", + "dismissed", + "promoted", + "closed" + ] + }, + "public.approval_status": { + "name": "approval_status", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected", + "expired", + "cancelled", + "executed", + "failed" + ] + }, + "public.investigation_status": { + "name": "investigation_status", + "schema": "public", + "values": [ + "open", + "triaging", + "investigating", + "awaiting_approval", + "promoted", + "closed" + ] + }, + "public.message_type": { + "name": "message_type", + "schema": "public", + "values": [ + "text", + "system", + "alert", + "finding", + "decision", + "approval", + "workflow", + "agent-status", + "query-result", + "evidence", + "case-event", + "response-action" + ] + }, + "public.room_type": { + "name": "room_type", + "schema": "public", + "values": [ + "operations", + "incident", + "investigation", + "hunt", + "engineering", + "private", + "direct", + "system" + ] + }, + "public.severity": { + "name": "severity", + "schema": "public", + "values": [ + "critical", + "high", + "medium", + "low", + "informational" + ] + }, + "public.task_priority": { + "name": "task_priority", + "schema": "public", + "values": [ + "urgent", + "high", + "normal", + "low" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "backlog", + "ready", + "in_progress", + "review", + "done" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index 80d036a..3cc902a 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -127,6 +127,20 @@ "when": 1785126003887, "tag": "0017_daily_dexter_bennett", "breakpoints": true + }, + { + "idx": 18, + "version": "7", + "when": 1785126258301, + "tag": "0018_watery_morg", + "breakpoints": true + }, + { + "idx": 19, + "version": "7", + "when": 1785127721571, + "tag": "0019_fuzzy_zemo", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/database/package.json b/packages/database/package.json index 02e5aa4..d059000 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -29,12 +29,12 @@ "migrate": "tsx src/migrate.ts", "bootstrap": "tsx src/bootstrap.ts", "verify-clean": "tsx src/verify-clean-install.ts", - "cleanup:synthetic": "tsx src/synthetic-cleanup.ts", "verify:audit": "tsx src/verify-audit-integrity.ts", "seed": "tsx src/seed.ts" }, "dependencies": { "@muster/audit": "workspace:*", + "@muster/authz": "workspace:*", "@muster/contracts": "workspace:*", "drizzle-orm": "0.45.2", "pg": "^8.16.3", diff --git a/packages/database/src/schema.ts b/packages/database/src/schema.ts index 9d57382..d69b4a7 100644 --- a/packages/database/src/schema.ts +++ b/packages/database/src/schema.ts @@ -1971,9 +1971,15 @@ export const researchWatchlists = pgTable( "research_watchlists", { id: uuid("id").primaryKey(), - organisationId: uuid("organisation_id").notNull().references(() => organisations.id), - roomId: uuid("room_id").notNull().references(() => rooms.id), - createdByActorId: uuid("created_by_actor_id").notNull().references(() => actors.id), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + roomId: uuid("room_id") + .notNull() + .references(() => rooms.id), + createdByActorId: uuid("created_by_actor_id") + .notNull() + .references(() => actors.id), name: text("name").notNull(), vendors: jsonb("vendors").notNull().default([]), technologies: jsonb("technologies").notNull().default([]), @@ -1986,9 +1992,15 @@ export const researchWatchlists = pgTable( ...timestamps, }, (table) => [ - uniqueIndex("research_watchlists_org_name_unique").on(table.organisationId, table.name), + uniqueIndex("research_watchlists_org_name_unique").on( + table.organisationId, + table.name, + ), index("research_watchlists_due_idx").on(table.enabled, table.nextRunAt), - check("research_watchlists_cadence_check", sql`${table.cadenceMinutes} between 15 and 10080`), + check( + "research_watchlists_cadence_check", + sql`${table.cadenceMinutes} between 15 and 10080`, + ), ], ); @@ -1996,9 +2008,15 @@ export const researchRuns = pgTable( "research_runs", { id: uuid("id").primaryKey(), - organisationId: uuid("organisation_id").notNull().references(() => organisations.id), - watchlistId: uuid("watchlist_id").notNull().references(() => researchWatchlists.id), - agentRunId: uuid("agent_run_id").notNull().references(() => agentRuns.id), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + watchlistId: uuid("watchlist_id") + .notNull() + .references(() => researchWatchlists.id), + agentRunId: uuid("agent_run_id") + .notNull() + .references(() => agentRuns.id), status: text("status").notNull().default("queued"), sourceLimit: integer("source_limit").notNull(), tokenBudget: integer("token_budget").notNull(), @@ -2010,10 +2028,23 @@ export const researchRuns = pgTable( ...timestamps, }, (table) => [ - uniqueIndex("research_runs_org_idempotency_unique").on(table.organisationId, table.idempotencyKey), - uniqueIndex("research_runs_org_agent_unique").on(table.organisationId, table.agentRunId), - index("research_runs_org_status_idx").on(table.organisationId, table.status, table.createdAt), - check("research_runs_status_check", sql`${table.status} in ('queued','running','completed','failed')`), + uniqueIndex("research_runs_org_idempotency_unique").on( + table.organisationId, + table.idempotencyKey, + ), + uniqueIndex("research_runs_org_agent_unique").on( + table.organisationId, + table.agentRunId, + ), + index("research_runs_org_status_idx").on( + table.organisationId, + table.status, + table.createdAt, + ), + check( + "research_runs_status_check", + sql`${table.status} in ('queued','running','completed','failed')`, + ), ], ); @@ -2021,9 +2052,15 @@ export const researchItems = pgTable( "research_items", { id: uuid("id").primaryKey(), - organisationId: uuid("organisation_id").notNull().references(() => organisations.id), - watchlistId: uuid("watchlist_id").notNull().references(() => researchWatchlists.id), - researchRunId: uuid("research_run_id").notNull().references(() => researchRuns.id), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + watchlistId: uuid("watchlist_id") + .notNull() + .references(() => researchWatchlists.id), + researchRunId: uuid("research_run_id") + .notNull() + .references(() => researchRuns.id), fingerprint: text("fingerprint").notNull(), sourceUrl: text("source_url").notNull(), sourcePublishedAt: timestamp("source_published_at", { withTimezone: true }), @@ -2036,8 +2073,15 @@ export const researchItems = pgTable( ...timestamps, }, (table) => [ - uniqueIndex("research_items_org_fingerprint_unique").on(table.organisationId, table.fingerprint), - index("research_items_org_watchlist_idx").on(table.organisationId, table.watchlistId, table.updatedAt), + uniqueIndex("research_items_org_fingerprint_unique").on( + table.organisationId, + table.fingerprint, + ), + index("research_items_org_watchlist_idx").on( + table.organisationId, + table.watchlistId, + table.updatedAt, + ), ], ); @@ -2047,11 +2091,17 @@ export const reportManifests = pgTable( "report_manifests", { id: uuid("id").primaryKey(), - organisationId: uuid("organisation_id").notNull().references(() => organisations.id), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), agentRunId: uuid("agent_run_id").references(() => agentRuns.id), taskId: uuid("task_id").references(() => tasks.id), - roomId: uuid("room_id").notNull().references(() => rooms.id), - requestedByActorId: uuid("requested_by_actor_id").notNull().references(() => actors.id), + roomId: uuid("room_id") + .notNull() + .references(() => rooms.id), + requestedByActorId: uuid("requested_by_actor_id") + .notNull() + .references(() => actors.id), version: integer("version").notNull().default(1), status: text("status").notNull().default("draft"), manifest: jsonb("manifest").notNull(), @@ -2064,9 +2114,20 @@ export const reportManifests = pgTable( ...timestamps, }, (table) => [ - uniqueIndex("report_manifests_org_idempotency_unique").on(table.organisationId, table.idempotencyKey), - index("report_manifests_org_room_status_idx").on(table.organisationId, table.roomId, table.status, table.createdAt), - check("report_manifests_status_check", sql`${table.status} in ('draft','reviewed','posted','superseded')`), + uniqueIndex("report_manifests_org_idempotency_unique").on( + table.organisationId, + table.idempotencyKey, + ), + index("report_manifests_org_room_status_idx").on( + table.organisationId, + table.roomId, + table.status, + table.createdAt, + ), + check( + "report_manifests_status_check", + sql`${table.status} in ('draft','reviewed','posted','superseded')`, + ), check("report_manifests_version_check", sql`${table.version} > 0`), ], ); @@ -2075,10 +2136,18 @@ export const reportDeliveries = pgTable( "report_deliveries", { id: uuid("id").primaryKey(), - organisationId: uuid("organisation_id").notNull().references(() => organisations.id), - reportId: uuid("report_id").notNull().references(() => reportManifests.id), - approvalId: uuid("approval_id").notNull().references(() => approvals.id), - requestedByActorId: uuid("requested_by_actor_id").notNull().references(() => actors.id), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + reportId: uuid("report_id") + .notNull() + .references(() => reportManifests.id), + approvalId: uuid("approval_id") + .notNull() + .references(() => approvals.id), + requestedByActorId: uuid("requested_by_actor_id") + .notNull() + .references(() => actors.id), recipient: text("recipient").notNull(), status: text("status").notNull().default("awaiting_approval"), result: jsonb("result"), @@ -2087,9 +2156,19 @@ export const reportDeliveries = pgTable( ...timestamps, }, (table) => [ - uniqueIndex("report_deliveries_org_idempotency_unique").on(table.organisationId, table.idempotencyKey), - index("report_deliveries_org_report_status_idx").on(table.organisationId, table.reportId, table.status), - check("report_deliveries_status_check", sql`${table.status} in ('awaiting_approval','queued','delivered','failed','cancelled')`), + uniqueIndex("report_deliveries_org_idempotency_unique").on( + table.organisationId, + table.idempotencyKey, + ), + index("report_deliveries_org_report_status_idx").on( + table.organisationId, + table.reportId, + table.status, + ), + check( + "report_deliveries_status_check", + sql`${table.status} in ('awaiting_approval','queued','delivered','failed','cancelled')`, + ), ], ); @@ -2097,9 +2176,15 @@ export const reportSchedules = pgTable( "report_schedules", { id: uuid("id").primaryKey(), - organisationId: uuid("organisation_id").notNull().references(() => organisations.id), - roomId: uuid("room_id").notNull().references(() => rooms.id), - createdByActorId: uuid("created_by_actor_id").notNull().references(() => actors.id), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + roomId: uuid("room_id") + .notNull() + .references(() => rooms.id), + createdByActorId: uuid("created_by_actor_id") + .notNull() + .references(() => actors.id), cadence: text("cadence").notNull(), timezone: text("timezone").notNull(), audience: text("audience").notNull().default("leadership"), @@ -2111,10 +2196,23 @@ export const reportSchedules = pgTable( ...timestamps, }, (table) => [ - uniqueIndex("report_schedules_org_idempotency_unique").on(table.organisationId, table.idempotencyKey), - index("report_schedules_org_due_idx").on(table.organisationId, table.enabled, table.nextRunAt), - check("report_schedules_cadence_check", sql`${table.cadence} in ('weekly','monthly')`), - check("report_schedules_audience_check", sql`${table.audience} in ('analyst','leadership','executive')`), + uniqueIndex("report_schedules_org_idempotency_unique").on( + table.organisationId, + table.idempotencyKey, + ), + index("report_schedules_org_due_idx").on( + table.organisationId, + table.enabled, + table.nextRunAt, + ), + check( + "report_schedules_cadence_check", + sql`${table.cadence} in ('weekly','monthly')`, + ), + check( + "report_schedules_audience_check", + sql`${table.audience} in ('analyst','leadership','executive')`, + ), ], ); @@ -2140,6 +2238,114 @@ export const idempotencyRecords = pgTable( ], ); +export const syntheticArtifactProvenance = pgTable( + "synthetic_artifact_provenance", + { + id: uuid("id").primaryKey(), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + artifactTable: text("artifact_table").notNull(), + artifactId: uuid("artifact_id").notNull(), + sourceKind: text("source_kind").notNull(), + sourceReference: text("source_reference").notNull(), + recordedByActorId: uuid("recorded_by_actor_id") + .notNull() + .references(() => actors.id), + recordedAt: timestamp("recorded_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }, + (table) => [ + uniqueIndex("synthetic_artifact_provenance_artifact_unique").on( + table.organisationId, + table.artifactTable, + table.artifactId, + ), + check( + "synthetic_artifact_provenance_table_check", + sql`${table.artifactTable} in ('rooms','tasks','hunts','integrations','researchWatchlists','reportManifests','reportSchedules','messages','evidence','agentMemories','actors')`, + ), + check( + "synthetic_artifact_provenance_source_check", + sql`${table.sourceKind} in ('seed_fixture','mock_runtime','test_fixture','legacy_live_proof')`, + ), + ], +); + +export const syntheticCleanupReceipts = pgTable( + "synthetic_cleanup_receipts", + { + manifestId: uuid("manifest_id").primaryKey(), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + approvalId: uuid("approval_id") + .notNull() + .references(() => approvals.id), + maintenanceActorId: uuid("maintenance_actor_id") + .notNull() + .references(() => actors.id), + manifestDigest: text("manifest_digest").notNull(), + manifest: jsonb("manifest").notNull(), + candidateCounts: jsonb("candidate_counts").notNull(), + preDigests: jsonb("pre_digests").notNull(), + postDigests: jsonb("post_digests").notNull(), + objectStorageObjects: jsonb("object_storage_objects").notNull().default([]), + traceId: text("trace_id").notNull(), + appliedAt: timestamp("applied_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }, + (table) => [ + uniqueIndex("synthetic_cleanup_receipts_approval_unique").on( + table.approvalId, + ), + uniqueIndex("synthetic_cleanup_receipts_org_digest_unique").on( + table.organisationId, + table.manifestDigest, + ), + ], +); + +export const syntheticCleanupObjectDeletionAttempts = pgTable( + "synthetic_cleanup_object_deletion_attempts", + { + id: uuid("id").primaryKey(), + manifestId: uuid("manifest_id") + .notNull() + .references(() => syntheticCleanupReceipts.manifestId), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + evidenceId: uuid("evidence_id").notNull(), + versionId: text("version_id").notNull(), + authorizationApprovalId: uuid("authorization_approval_id") + .notNull() + .references(() => approvals.id), + result: text("result").notNull(), + errorCode: text("error_code"), + attemptedByActorId: uuid("attempted_by_actor_id") + .notNull() + .references(() => actors.id), + traceId: text("trace_id").notNull(), + attemptedAt: timestamp("attempted_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }, + (table) => [ + index("synthetic_cleanup_object_attempts_manifest_idx").on( + table.organisationId, + table.manifestId, + table.attemptedAt, + ), + check( + "synthetic_cleanup_object_attempts_result_check", + sql`${table.result} in ('started','succeeded','failed','observed_missing')`, + ), + ], +); + export const outboxEvents = pgTable( "outbox_events", { diff --git a/packages/database/src/synthetic-cleanup.integration.test.ts b/packages/database/src/synthetic-cleanup.integration.test.ts new file mode 100644 index 0000000..90e188f --- /dev/null +++ b/packages/database/src/synthetic-cleanup.integration.test.ts @@ -0,0 +1,932 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { and, eq, inArray } from "drizzle-orm"; +import { + applySyntheticCleanup, + authoriseSyntheticCleanupObjectRetry, + captureSyntheticCleanupManifest, + parseSyntheticCleanupManifest, + requestSyntheticCleanupApproval, + requestSyntheticCleanupObjectRetryApproval, + recordSyntheticCleanupObjectDeletionAttempt, + type SyntheticCleanupPlan, +} from "./synthetic-cleanup.ts"; +import { closeDatabase, database } from "./index.ts"; +import * as schema from "./schema.ts"; + +const integration = process.env.MUSTER_INTEGRATION_TESTS === "true"; +const describeIntegration = integration ? describe.sequential : describe.skip; + +const organisationId = "019fa300-0000-7000-8000-000000000001"; +const otherOrganisationId = "019fa300-0000-7000-8000-000000000002"; +const maintenanceActorId = "019fa300-0000-7000-8000-000000000003"; +const approverActorId = "019fa300-0000-7000-8000-000000000004"; +const syntheticActorId = "019fa300-0000-7000-8000-000000000005"; +const directRoomId = "019fa300-0000-7000-8000-000000000006"; +const candidateRoomId = "019fa300-0000-7000-8000-000000000007"; +const candidateTaskId = "019fa300-0000-7000-8000-000000000008"; +const heldEvidenceId = "019fa300-0000-7000-8000-000000000009"; +const candidateEvidenceId = "019fa300-0000-7000-8000-000000000010"; +const otherRoomId = "019fa300-0000-7000-8000-000000000011"; +const guardedRoomId = "019fa300-0000-7000-8000-000000000013"; +const protectedAgentId = "019fa300-0000-7000-8000-000000000014"; +const otherHumanActorId = "019fa300-0000-7000-8000-000000000012"; +const unprovenRoomId = "019fa300-0000-7000-8000-000000000015"; +const provenance = { + candidateRoom: "019fa303-0000-7000-8000-000000000001", + guardedRoom: "019fa303-0000-7000-8000-000000000002", + candidateTask: "019fa303-0000-7000-8000-000000000003", + heldEvidence: "019fa303-0000-7000-8000-000000000004", + candidateEvidence: "019fa303-0000-7000-8000-000000000005", + syntheticActor: "019fa303-0000-7000-8000-000000000006", +} as const; +const protectedMessageIds = [ + "019fa05a-fff0-76ce-9084-bf0707206d15", + "019fa05b-c62c-7368-8166-a23b68e3057f", + "019fa19f-335e-708e-9ce3-be4083921691", + "019fa19f-5c96-7402-8784-0324bb98d48c", +] as const; +const operatorSubject = { + actorId: maintenanceActorId, + organisationId, + capabilities: new Set(["administration.manage"] as const), +}; + +function basePlan( + suffix: string, + overrides: Partial = {}, +): SyntheticCleanupPlan { + return { + version: 2, + manifestId: `019fa301-0000-7000-8000-${suffix.padStart(12, "0")}`, + approvalId: `019fa302-0000-7000-8000-${suffix.padStart(12, "0")}`, + organisationId, + maintenanceActorId, + generatedAt: "2026-07-27T00:00:00.000Z", + archiveRoomIds: [], + archiveTaskIds: [], + archiveHuntIds: [], + archiveIntegrationIds: [], + archiveResearchWatchlistIds: [], + archiveReportManifestIds: [], + archiveReportScheduleIds: [], + hideMessageIds: [], + retireEvidenceIds: [], + rejectAgentMemoryIds: [], + retireActorIds: [], + selectionEvidence: [], + objectStorageObjects: [], + ...overrides, + }; +} + +describeIntegration("synthetic cleanup transaction", () => { + beforeAll(async () => { + const db = database(); + await db.insert(schema.organisations).values([ + { + id: organisationId, + name: "Synthetic Cleanup Integration", + slug: `synthetic-cleanup-${organisationId}`, + }, + { + id: otherOrganisationId, + name: "Protected Other Organisation", + slug: `synthetic-cleanup-${otherOrganisationId}`, + }, + ]); + await db.insert(schema.actors).values([ + { + id: maintenanceActorId, + organisationId, + actorType: "human", + displayName: "Synthetic Maintenance Operator", + identityReference: `synthetic-maintenance:${maintenanceActorId}`, + capabilityAssignments: ["administration.manage"], + }, + { + id: approverActorId, + organisationId, + actorType: "human", + displayName: "Synthetic Cleanup Approver", + identityReference: `synthetic-approver:${approverActorId}`, + capabilityAssignments: ["administration.manage"], + }, + { + id: syntheticActorId, + organisationId, + actorType: "human", + displayName: "Synthetic Retired User", + identityReference: `synthetic-user:${syntheticActorId}`, + capabilityAssignments: [], + }, + { + id: protectedAgentId, + organisationId: otherOrganisationId, + actorType: "agent", + displayName: "Protected Synthetic Agent", + identityReference: `agent:protected-fixture:${protectedAgentId}`, + capabilityAssignments: [], + }, + { + id: otherHumanActorId, + organisationId: otherOrganisationId, + actorType: "human", + displayName: "Protected Other User", + capabilityAssignments: [], + }, + ]); + await db.insert(schema.rooms).values([ + { + id: directRoomId, + organisationId: otherOrganisationId, + name: `protected-direct-${directRoomId}`, + slug: `protected-direct-${directRoomId}`, + displayName: "Protected direct room", + roomType: "direct", + visibility: "private", + createdByActorId: otherHumanActorId, + }, + { + id: candidateRoomId, + organisationId, + name: `synthetic-room-${candidateRoomId}`, + slug: `synthetic-room-${candidateRoomId}`, + displayName: "Synthetic cleanup room", + roomType: "operations", + createdByActorId: maintenanceActorId, + }, + { + id: guardedRoomId, + organisationId, + name: `synthetic-guarded-room-${guardedRoomId}`, + slug: `synthetic-guarded-room-${guardedRoomId}`, + displayName: "Synthetic guarded cleanup room", + roomType: "operations", + createdByActorId: maintenanceActorId, + }, + { + id: unprovenRoomId, + organisationId, + name: `demo-customer-room-${unprovenRoomId}`, + slug: `demo-customer-room-${unprovenRoomId}`, + displayName: "Genuine customer test coordination", + roomType: "operations", + visibility: "private", + createdByActorId: maintenanceActorId, + }, + ]); + await db.insert(schema.messages).values( + protectedMessageIds.map((id) => ({ + id, + organisationId: otherOrganisationId, + roomId: directRoomId, + authorActorId: otherHumanActorId, + messageType: "text" as const, + document: { type: "doc", content: [] }, + plainText: `Protected genuine fixture ${id}`, + idempotencyKey: `protected-fixture:${id}`, + })), + ); + await db.insert(schema.roomMemberships).values([ + { + organisationId: otherOrganisationId, + roomId: directRoomId, + actorId: otherHumanActorId, + membershipRole: "owner", + }, + { + organisationId: otherOrganisationId, + roomId: directRoomId, + actorId: protectedAgentId, + membershipRole: "agent_member", + }, + ]); + await db.insert(schema.tasks).values({ + id: candidateTaskId, + organisationId, + title: "Synthetic completed cleanup task", + status: "done", + createdByActorId: maintenanceActorId, + idempotencyKey: `synthetic-cleanup-task:${candidateTaskId}`, + completedAt: new Date("2026-07-27T00:00:00.000Z"), + }); + await db.insert(schema.evidence).values([ + { + id: heldEvidenceId, + organisationId, + fileName: "synthetic-held.bin", + mimeType: "application/octet-stream", + size: 1, + sha256: "b".repeat(64), + uploadedByActorId: maintenanceActorId, + classification: "internal", + source: "synthetic integration fixture", + storageKey: `synthetic/${heldEvidenceId}`, + legalHold: true, + }, + { + id: candidateEvidenceId, + organisationId, + fileName: "synthetic-candidate.bin", + mimeType: "application/octet-stream", + size: 1, + sha256: "c".repeat(64), + uploadedByActorId: maintenanceActorId, + classification: "internal", + source: "synthetic integration fixture", + storageKey: `synthetic/${candidateEvidenceId}`, + }, + ]); + await db.insert(schema.rooms).values({ + id: otherRoomId, + organisationId: otherOrganisationId, + name: `protected-other-${otherRoomId}`, + slug: `protected-other-${otherRoomId}`, + displayName: "Protected other room", + roomType: "operations", + createdByActorId: otherHumanActorId, + }); + await db.insert(schema.syntheticArtifactProvenance).values([ + { + id: provenance.candidateRoom, + organisationId, + artifactTable: "rooms", + artifactId: candidateRoomId, + sourceKind: "test_fixture", + sourceReference: "synthetic-cleanup-integration", + recordedByActorId: maintenanceActorId, + }, + { + id: provenance.guardedRoom, + organisationId, + artifactTable: "rooms", + artifactId: guardedRoomId, + sourceKind: "test_fixture", + sourceReference: "synthetic-cleanup-integration", + recordedByActorId: maintenanceActorId, + }, + { + id: provenance.candidateTask, + organisationId, + artifactTable: "tasks", + artifactId: candidateTaskId, + sourceKind: "test_fixture", + sourceReference: "synthetic-cleanup-integration", + recordedByActorId: maintenanceActorId, + }, + { + id: provenance.heldEvidence, + organisationId, + artifactTable: "evidence", + artifactId: heldEvidenceId, + sourceKind: "test_fixture", + sourceReference: "synthetic-cleanup-integration", + recordedByActorId: maintenanceActorId, + }, + { + id: provenance.candidateEvidence, + organisationId, + artifactTable: "evidence", + artifactId: candidateEvidenceId, + sourceKind: "test_fixture", + sourceReference: "synthetic-cleanup-integration", + recordedByActorId: maintenanceActorId, + }, + { + id: provenance.syntheticActor, + organisationId, + artifactTable: "actors", + artifactId: syntheticActorId, + sourceKind: "test_fixture", + sourceReference: "synthetic-cleanup-integration", + recordedByActorId: maintenanceActorId, + }, + ]); + }); + + afterAll(closeDatabase); + + it("fails closed for cross-organisation and legal-held candidates", async () => { + await expect( + captureSyntheticCleanupManifest( + operatorSubject, + basePlan("1", { + archiveRoomIds: [otherRoomId], + selectionEvidence: [ + { + table: "rooms", + recordId: otherRoomId, + provenanceId: provenance.candidateRoom, + }, + ], + }), + ), + ).rejects.toThrow("candidate count or ownership changed"); + + await expect( + captureSyntheticCleanupManifest( + operatorSubject, + basePlan("2", { + retireEvidenceIds: [heldEvidenceId], + selectionEvidence: [ + { + table: "evidence", + recordId: heldEvidenceId, + provenanceId: provenance.heldEvidence, + }, + ], + objectStorageObjects: [ + { + evidenceId: heldEvidenceId, + bucket: "synthetic-evidence", + key: `synthetic/${heldEvidenceId}`, + versionId: "synthetic-version-held", + etag: "synthetic-etag-held", + size: 1, + sha256: "b".repeat(64), + legalHold: false, + objectLockMetadata: {}, + }, + ], + }), + ), + ).rejects.toThrow("held or already retired"); + + await expect( + captureSyntheticCleanupManifest( + operatorSubject, + basePlan("5", { + archiveRoomIds: [unprovenRoomId], + selectionEvidence: [ + { + table: "rooms", + recordId: unprovenRoomId, + provenanceId: provenance.guardedRoom, + }, + ], + }), + ), + ).rejects.toThrow("lacks exact append-only synthetic provenance"); + + const [held] = await database() + .select({ state: schema.evidence.retentionState }) + .from(schema.evidence) + .where(eq(schema.evidence.id, heldEvidenceId)); + expect(held?.state).toBe("active"); + }); + + it("does not mutate for pending, stale, or capability-revoked approval", async () => { + const manifest = await captureSyntheticCleanupManifest( + operatorSubject, + basePlan("4", { + archiveRoomIds: [guardedRoomId], + selectionEvidence: [ + { + table: "rooms", + recordId: guardedRoomId, + provenanceId: provenance.guardedRoom, + }, + ], + }), + ); + const otherAdminSubject = { + actorId: approverActorId, + organisationId, + capabilities: new Set(["administration.manage"] as const), + }; + await expect( + requestSyntheticCleanupApproval( + otherAdminSubject, + manifest, + "trace-cleanup-impersonated-request", + ), + ).rejects.toThrow("Missing capability"); + await requestSyntheticCleanupApproval( + operatorSubject, + manifest, + "trace-cleanup-guard-request", + ); + await expect( + applySyntheticCleanup( + otherAdminSubject, + manifest, + "trace-cleanup-impersonated-apply", + ), + ).rejects.toThrow("Missing capability"); + await expect( + applySyntheticCleanup(operatorSubject, manifest, "trace-cleanup-pending"), + ).rejects.toThrow("approval is missing"); + + await database() + .update(schema.approvals) + .set({ + status: "approved", + decisions: [ + { + actorId: maintenanceActorId, + status: "approved", + reason: "Invalid self approval", + decidedAt: "2026-07-27T00:01:00.000Z", + }, + ], + }) + .where(eq(schema.approvals.id, manifest.approvalId)); + await expect( + applySyntheticCleanup( + operatorSubject, + manifest, + "trace-cleanup-self-approved", + ), + ).rejects.toThrow("independent approver"); + + await database() + .update(schema.approvals) + .set({ + decisions: [ + { + actorId: approverActorId, + status: "approved", + reason: "Exact guarded fixture reviewed", + decidedAt: "2026-07-27T00:02:00.000Z", + }, + ], + }) + .where(eq(schema.approvals.id, manifest.approvalId)); + await database() + .update(schema.actors) + .set({ capabilityAssignments: [] }) + .where(eq(schema.actors.id, approverActorId)); + await expect( + applySyntheticCleanup(operatorSubject, manifest, "trace-cleanup-revoked"), + ).rejects.toThrow("capability was revoked"); + + await database() + .update(schema.actors) + .set({ capabilityAssignments: ["administration.manage"] }) + .where(eq(schema.actors.id, approverActorId)); + await database() + .update(schema.approvals) + .set({ + target: { manifestId: manifest.manifestId, digest: "d".repeat(64) }, + }) + .where(eq(schema.approvals.id, manifest.approvalId)); + await expect( + applySyntheticCleanup( + operatorSubject, + manifest, + "trace-cleanup-target-drift", + ), + ).rejects.toThrow("approval is missing"); + + await database() + .update(schema.approvals) + .set({ + target: { + manifestId: manifest.manifestId, + digest: manifest.digest, + }, + }) + .where(eq(schema.approvals.id, manifest.approvalId)); + await database() + .update(schema.rooms) + .set({ topic: "Changed after exact manifest capture" }) + .where(eq(schema.rooms.id, guardedRoomId)); + await expect( + applySyntheticCleanup( + operatorSubject, + manifest, + "trace-cleanup-prestate-drift", + ), + ).rejects.toThrow("pre-state digest changed"); + await database() + .update(schema.rooms) + .set({ topic: "" }) + .where(eq(schema.rooms.id, guardedRoomId)); + await database() + .update(schema.approvals) + .set({ expiresAt: new Date("2026-07-26T00:00:00.000Z") }) + .where(eq(schema.approvals.id, manifest.approvalId)); + await expect( + applySyntheticCleanup(operatorSubject, manifest, "trace-cleanup-expired"), + ).rejects.toThrow("approval is missing"); + + const [room, receipt] = await Promise.all([ + database() + .select({ archivedAt: schema.rooms.archivedAt }) + .from(schema.rooms) + .where(eq(schema.rooms.id, guardedRoomId)), + database() + .select() + .from(schema.syntheticCleanupReceipts) + .where( + eq(schema.syntheticCleanupReceipts.manifestId, manifest.manifestId), + ), + ]); + expect(room[0]?.archivedAt).toBeNull(); + expect(receipt).toHaveLength(0); + }); + + it("applies the exact approved manifest once and preserves receipt/history", async () => { + const manifest = await captureSyntheticCleanupManifest( + operatorSubject, + basePlan("3", { + archiveRoomIds: [candidateRoomId], + archiveTaskIds: [candidateTaskId], + retireEvidenceIds: [candidateEvidenceId], + retireActorIds: [syntheticActorId], + selectionEvidence: [ + { + table: "rooms", + recordId: candidateRoomId, + provenanceId: provenance.candidateRoom, + }, + { + table: "tasks", + recordId: candidateTaskId, + provenanceId: provenance.candidateTask, + }, + { + table: "evidence", + recordId: candidateEvidenceId, + provenanceId: provenance.candidateEvidence, + }, + { + table: "actors", + recordId: syntheticActorId, + provenanceId: provenance.syntheticActor, + }, + ], + objectStorageObjects: [ + { + evidenceId: candidateEvidenceId, + bucket: "synthetic-evidence", + key: `synthetic/${candidateEvidenceId}`, + versionId: "synthetic-version-1", + etag: "synthetic-etag", + size: 1, + sha256: "c".repeat(64), + legalHold: false, + objectLockMetadata: {}, + }, + ], + }), + ); + const requested = await requestSyntheticCleanupApproval( + operatorSubject, + manifest, + "trace-cleanup-request", + ); + expect(requested).toMatchObject({ requested: true }); + const duplicateRequest = await requestSyntheticCleanupApproval( + operatorSubject, + manifest, + "trace-cleanup-request-replay", + ); + expect(duplicateRequest).toMatchObject({ requested: false }); + + await database() + .update(schema.approvals) + .set({ + status: "approved", + decisions: [ + { + actorId: approverActorId, + status: "approved", + reason: "Exact synthetic fixture reviewed", + decidedAt: "2026-07-27T00:01:00.000Z", + }, + ], + decisionAt: new Date("2026-07-27T00:01:00.000Z"), + }) + .where( + and( + eq(schema.approvals.organisationId, organisationId), + eq(schema.approvals.id, manifest.approvalId), + ), + ); + + const concurrentResults = await Promise.all([ + applySyntheticCleanup(operatorSubject, manifest, "trace-cleanup-apply-a"), + applySyntheticCleanup(operatorSubject, manifest, "trace-cleanup-apply-b"), + ]); + expect(concurrentResults.map((result) => result.applied).sort()).toEqual([ + false, + true, + ]); + expect(concurrentResults.find((result) => result.applied)).toMatchObject({ + applied: true, + candidateCounts: { + rooms: 1, + tasks: 1, + evidence: 1, + actors: 1, + }, + }); + + const [ + room, + task, + evidence, + actor, + approval, + receipts, + audits, + outbox, + protectedMessages, + ] = await Promise.all([ + database() + .select({ archivedAt: schema.rooms.archivedAt }) + .from(schema.rooms) + .where(eq(schema.rooms.id, candidateRoomId)), + database() + .select({ archivedAt: schema.tasks.archivedAt }) + .from(schema.tasks) + .where(eq(schema.tasks.id, candidateTaskId)), + database() + .select({ state: schema.evidence.retentionState }) + .from(schema.evidence) + .where(eq(schema.evidence.id, candidateEvidenceId)), + database() + .select({ status: schema.actors.status }) + .from(schema.actors) + .where(eq(schema.actors.id, syntheticActorId)), + database() + .select({ + status: schema.approvals.status, + executedAt: schema.approvals.executedAt, + }) + .from(schema.approvals) + .where(eq(schema.approvals.id, manifest.approvalId)), + database() + .select() + .from(schema.syntheticCleanupReceipts) + .where( + eq(schema.syntheticCleanupReceipts.manifestId, manifest.manifestId), + ), + database() + .select() + .from(schema.auditEvents) + .where( + and( + eq(schema.auditEvents.organisationId, organisationId), + eq(schema.auditEvents.targetId, manifest.manifestId), + ), + ), + database() + .select() + .from(schema.outboxEvents) + .where( + and( + eq(schema.outboxEvents.organisationId, organisationId), + eq(schema.outboxEvents.aggregateId, manifest.manifestId), + ), + ), + database() + .select({ + id: schema.messages.id, + deletedAt: schema.messages.deletedAt, + }) + .from(schema.messages) + .where( + and( + eq(schema.messages.organisationId, otherOrganisationId), + inArray(schema.messages.id, [...protectedMessageIds]), + ), + ), + ]); + expect(room[0]?.archivedAt).toBeInstanceOf(Date); + expect(task[0]?.archivedAt).toBeInstanceOf(Date); + expect(evidence[0]?.state).toBe("retired"); + expect(actor[0]?.status).toBe("inactive"); + expect(approval[0]).toMatchObject({ status: "executed" }); + expect(approval[0]?.executedAt).toBeInstanceOf(Date); + expect(receipts).toHaveLength(1); + expect(audits).toHaveLength(2); + expect(outbox).toHaveLength(3); + expect(outbox).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + queueName: "muster-maintenance", + eventType: "maintenance.synthetic_cleanup.object_delete.queued", + aggregateId: manifest.manifestId, + }), + ]), + ); + expect(protectedMessages).toHaveLength(4); + expect( + protectedMessages.every((message) => message.deletedAt === null), + ).toBe(true); + + let mutationError: unknown; + try { + await database() + .update(schema.syntheticCleanupReceipts) + .set({ traceId: "tampered" }) + .where( + eq(schema.syntheticCleanupReceipts.manifestId, manifest.manifestId), + ); + } catch (error) { + mutationError = error; + } + expect(mutationError).toBeInstanceOf(Error); + expect( + `${String(mutationError)} ${String( + (mutationError as { cause?: unknown }).cause, + )}`, + ).toContain("append-only"); + let deleteError: unknown; + try { + await database() + .delete(schema.syntheticCleanupReceipts) + .where( + eq(schema.syntheticCleanupReceipts.manifestId, manifest.manifestId), + ); + } catch (error) { + deleteError = error; + } + expect( + `${String(deleteError)} ${String( + (deleteError as { cause?: unknown }).cause, + )}`, + ).toContain("append-only"); + + await database() + .insert(schema.syntheticCleanupObjectDeletionAttempts) + .values({ + id: "019fa304-0000-7000-8000-000000000001", + manifestId: manifest.manifestId, + organisationId, + evidenceId: candidateEvidenceId, + versionId: "synthetic-version-1", + authorizationApprovalId: manifest.approvalId, + result: "started", + attemptedByActorId: maintenanceActorId, + traceId: "trace-object-attempt", + }); + let attemptMutationError: unknown; + try { + await database() + .update(schema.syntheticCleanupObjectDeletionAttempts) + .set({ result: "succeeded" }) + .where( + eq( + schema.syntheticCleanupObjectDeletionAttempts.manifestId, + manifest.manifestId, + ), + ); + } catch (error) { + attemptMutationError = error; + } + expect( + `${String(attemptMutationError)} ${String( + (attemptMutationError as { cause?: unknown }).cause, + )}`, + ).toContain("append-only"); + + let provenanceMutationError: unknown; + try { + await database() + .delete(schema.syntheticArtifactProvenance) + .where( + eq(schema.syntheticArtifactProvenance.id, provenance.candidateRoom), + ); + } catch (error) { + provenanceMutationError = error; + } + expect( + `${String(provenanceMutationError)} ${String( + (provenanceMutationError as { cause?: unknown }).cause, + )}`, + ).toContain("append-only"); + }); + + it("requires a fresh independent approval to reconcile a pending object", async () => { + const [receipt] = await database() + .select({ manifest: schema.syntheticCleanupReceipts.manifest }) + .from(schema.syntheticCleanupReceipts) + .where( + eq( + schema.syntheticCleanupReceipts.manifestId, + "019fa301-0000-7000-8000-000000000003", + ), + ); + const manifest = parseSyntheticCleanupManifest(receipt?.manifest); + const retryApprovalId = "019fa302-0000-7000-8000-000000000099"; + const retry = { manifest, retryApprovalId }; + const requested = await requestSyntheticCleanupObjectRetryApproval( + operatorSubject, + retry, + "trace-object-retry-request", + ); + expect(requested).toMatchObject({ + requested: true, + pendingObjectVersions: 1, + }); + await expect( + authoriseSyntheticCleanupObjectRetry( + operatorSubject, + retry, + "trace-object-retry-pending", + ), + ).rejects.toThrow("approval is missing"); + await expect( + recordSyntheticCleanupObjectDeletionAttempt( + operatorSubject, + manifest, + manifest.objectStorageObjects[0]!, + retryApprovalId, + "observed_missing", + "trace-object-retry-unapproved-outcome", + ), + ).rejects.toThrow("approval is not executable"); + await database() + .update(schema.approvals) + .set({ + status: "approved", + decisions: [ + { + actorId: approverActorId, + status: "approved", + reason: "Exact pending immutable object version reviewed", + decidedAt: "2026-07-27T00:03:00.000Z", + }, + ], + decisionAt: new Date("2026-07-27T00:03:00.000Z"), + }) + .where(eq(schema.approvals.id, retryApprovalId)); + const authorised = await authoriseSyntheticCleanupObjectRetry( + operatorSubject, + retry, + "trace-object-retry-authorise", + ); + expect(authorised).toMatchObject({ + authorised: true, + pendingObjects: [{ evidenceId: candidateEvidenceId }], + }); + await recordSyntheticCleanupObjectDeletionAttempt( + operatorSubject, + manifest, + manifest.objectStorageObjects[0]!, + retryApprovalId, + "observed_missing", + "trace-object-retry-observed-missing", + ); + const [attempt, retryJob] = await Promise.all([ + database() + .select({ + authorizationApprovalId: + schema.syntheticCleanupObjectDeletionAttempts + .authorizationApprovalId, + result: schema.syntheticCleanupObjectDeletionAttempts.result, + }) + .from(schema.syntheticCleanupObjectDeletionAttempts) + .where( + and( + eq( + schema.syntheticCleanupObjectDeletionAttempts.manifestId, + manifest.manifestId, + ), + eq( + schema.syntheticCleanupObjectDeletionAttempts.result, + "observed_missing", + ), + ), + ), + database() + .select({ + aggregateType: schema.outboxEvents.aggregateType, + aggregateId: schema.outboxEvents.aggregateId, + queueName: schema.outboxEvents.queueName, + }) + .from(schema.outboxEvents) + .where( + and( + eq(schema.outboxEvents.organisationId, organisationId), + eq( + schema.outboxEvents.idempotencyKey, + `maintenance.synthetic-cleanup:${manifest.manifestId}:object-retry:${retryApprovalId}:queued`, + ), + ), + ), + ]); + expect(attempt[0]).toEqual({ + authorizationApprovalId: retryApprovalId, + result: "observed_missing", + }); + expect(retryJob[0]).toEqual({ + aggregateType: "cleanup_object_retry_approval", + aggregateId: retryApprovalId, + queueName: "muster-maintenance", + }); + await expect( + requestSyntheticCleanupObjectRetryApproval( + operatorSubject, + { + manifest, + retryApprovalId: "019fa302-0000-7000-8000-000000000100", + }, + "trace-object-retry-empty", + ), + ).rejects.toThrow("no pending object versions"); + }); +}); diff --git a/packages/database/src/synthetic-cleanup.test.ts b/packages/database/src/synthetic-cleanup.test.ts index 58423df..9e9ada2 100644 --- a/packages/database/src/synthetic-cleanup.test.ts +++ b/packages/database/src/synthetic-cleanup.test.ts @@ -3,35 +3,197 @@ import { parseSyntheticCleanupManifest, protectedDirectMessageIds, syntheticCleanupManifestDigest, + syntheticCleanupTableDigest, + syntheticCleanupTableKeys, type SyntheticCleanupManifest, } from "./synthetic-cleanup.ts"; +const emptyDigest = syntheticCleanupTableDigest([]); +const provenanceId = "019fa210-0000-7000-8000-000000000099"; const unsigned: Omit = { - version: 1, + version: 2, manifestId: "019fa210-0000-7000-8000-000000000001", - organisationId: "019fa210-0000-7000-8000-000000000002", - maintenanceActorId: "019fa210-0000-7000-8000-000000000003", + approvalId: "019fa210-0000-7000-8000-000000000002", + organisationId: "019fa210-0000-7000-8000-000000000003", + maintenanceActorId: "019fa210-0000-7000-8000-000000000004", generatedAt: "2026-07-27T00:00:00.000Z", - archiveRoomIds: [], hideMessageIds: [], retireEvidenceIds: [], - rejectAgentMemoryIds: [], disableWatchlistIds: [], tableDigests: {}, + archiveRoomIds: [], + archiveTaskIds: [], + archiveHuntIds: [], + archiveIntegrationIds: [], + archiveResearchWatchlistIds: [], + archiveReportManifestIds: [], + archiveReportScheduleIds: [], + hideMessageIds: [], + retireEvidenceIds: [], + rejectAgentMemoryIds: [], + retireActorIds: [], + selectionEvidence: [], + objectStorageObjects: [], + tableDigests: Object.fromEntries( + syntheticCleanupTableKeys.map((table) => [table, emptyDigest]), + ) as SyntheticCleanupManifest["tableDigests"], }; -function manifest(overrides: Partial = {}) { - const value = { ...unsigned, ...overrides } as Omit; +function manifest( + overrides: Partial> = {}, +) { + const value = { + ...unsigned, + ...overrides, + } as Omit; return { ...value, digest: syntheticCleanupManifestDigest(value) }; } describe("synthetic cleanup manifest", () => { - it("accepts exact, digest-bound candidates", () => { - expect(parseSyntheticCleanupManifest(manifest())).toMatchObject(unsigned); + it("accepts exact digest-bound candidates and proof", () => { + const roomId = "019fa210-0000-7000-8000-000000000005"; + const value = manifest({ + archiveRoomIds: [roomId], + selectionEvidence: [ + { + table: "rooms", + recordId: roomId, + provenanceId, + }, + ], + }); + + expect(parseSyntheticCleanupManifest(value)).toMatchObject({ + manifestId: unsigned.manifestId, + archiveRoomIds: [roomId], + }); }); it("rejects tampering", () => { - expect(() => parseSyntheticCleanupManifest({ ...manifest(), archiveRoomIds: ["019fa210-0000-7000-8000-000000000004"] })).toThrow("digest mismatch"); + expect(() => + parseSyntheticCleanupManifest({ + ...manifest(), + generatedAt: "2026-07-27T01:00:00.000Z", + }), + ).toThrow("digest mismatch"); + }); + + it("requires proof for every candidate and no unrelated proof", () => { + const roomId = "019fa210-0000-7000-8000-000000000005"; + expect(() => + parseSyntheticCleanupManifest(manifest({ archiveRoomIds: [roomId] })), + ).toThrow("exactly cover every candidate"); + + expect(() => + parseSyntheticCleanupManifest( + manifest({ + selectionEvidence: [ + { + table: "rooms", + recordId: roomId, + provenanceId, + }, + ], + }), + ), + ).toThrow("exactly cover every candidate"); }); it("preserves genuine direct messages", () => { - const value = manifest({ hideMessageIds: [protectedDirectMessageIds[0]] }); - expect(() => parseSyntheticCleanupManifest(value)).toThrow("protected direct message"); + const protectedId = protectedDirectMessageIds[0]; + const value = manifest({ + hideMessageIds: [protectedId], + selectionEvidence: [ + { + table: "messages", + recordId: protectedId, + provenanceId, + }, + ], + }); + expect(() => parseSyntheticCleanupManifest(value)).toThrow( + "protected direct message", + ); + }); + + it("cannot retire the authorised maintenance actor", () => { + const value = manifest({ + retireActorIds: [unsigned.maintenanceActorId], + selectionEvidence: [ + { + table: "actors", + recordId: unsigned.maintenanceActorId, + provenanceId, + }, + ], + }); + expect(() => parseSyntheticCleanupManifest(value)).toThrow( + "maintenance actor", + ); + }); + + it("binds object deletion inventory to selected evidence", () => { + const evidenceId = "019fa210-0000-7000-8000-000000000006"; + expect(() => + parseSyntheticCleanupManifest( + manifest({ + objectStorageObjects: [ + { + evidenceId, + bucket: "muster-evidence", + key: "org/evidence.bin", + versionId: "synthetic-version-1", + etag: "synthetic-etag", + size: 1, + sha256: "a".repeat(64), + legalHold: false, + objectLockMetadata: {}, + }, + ], + }), + ), + ).toThrow("selected evidence"); + }); + + it.each(["unversioned", "null"])( + "rejects mutable object version sentinel %s", + (versionId) => { + const evidenceId = "019fa210-0000-7000-8000-000000000006"; + expect(() => + parseSyntheticCleanupManifest( + manifest({ + retireEvidenceIds: [evidenceId], + selectionEvidence: [ + { table: "evidence", recordId: evidenceId, provenanceId }, + ], + objectStorageObjects: [ + { + evidenceId, + bucket: "muster-evidence", + key: "org/evidence.bin", + versionId, + etag: "synthetic-etag", + size: 1, + sha256: "a".repeat(64), + legalHold: false, + objectLockMetadata: {}, + }, + ], + }), + ), + ).toThrow("immutable object version"); + }, + ); + + it("hashes candidate rows independently of query order", () => { + const rows = [ + { + id: "019fa210-0000-7000-8000-000000000008", + updatedAt: new Date("2026-07-27T00:00:00.000Z"), + }, + { + id: "019fa210-0000-7000-8000-000000000007", + updatedAt: new Date("2026-07-26T00:00:00.000Z"), + }, + ]; + expect(syntheticCleanupTableDigest(rows)).toBe( + syntheticCleanupTableDigest([...rows].reverse()), + ); }); }); diff --git a/packages/database/src/synthetic-cleanup.ts b/packages/database/src/synthetic-cleanup.ts index 3506d85..7cd2dfb 100644 --- a/packages/database/src/synthetic-cleanup.ts +++ b/packages/database/src/synthetic-cleanup.ts @@ -1,10 +1,16 @@ import { createHash } from "node:crypto"; -import { readFile } from "node:fs/promises"; -import { and, eq, inArray } from "drizzle-orm"; +import { + actionApprovalPolicy, + assertExecutableApproval, + ForbiddenError, + requireCapability, + type AuthorisationSubject, +} from "@muster/authz"; +import { and, eq, getTableColumns, inArray, sql } from "drizzle-orm"; import { z } from "zod"; import { appendAuditEvent } from "./domain-transaction.ts"; import { newId } from "./ids.ts"; -import { closeDatabase, database } from "./index.ts"; +import { database } from "./index.ts"; import { writeOutbox } from "./outbox.ts"; import * as schema from "./schema.ts"; @@ -15,29 +21,124 @@ export const protectedDirectMessageIds = [ "019fa19f-5c96-7402-8784-0324bb98d48c", ] as const; -const ids = z.array(z.uuid()).max(10_000).refine( - (value) => new Set(value).size === value.length, - "Candidate IDs must be unique", -); - -export const SyntheticCleanupManifestSchema = z.object({ - version: z.literal(1), - manifestId: z.uuid(), - organisationId: z.uuid(), - maintenanceActorId: z.uuid(), - generatedAt: z.string().datetime({ offset: true }), - digest: z.string().regex(/^[a-f0-9]{64}$/), - archiveRoomIds: ids.default([]), - hideMessageIds: ids.default([]), - retireEvidenceIds: ids.default([]), - rejectAgentMemoryIds: ids.default([]), - disableWatchlistIds: ids.default([]), - tableDigests: z.record(z.string(), z.string().regex(/^[a-f0-9]{64}$/)), -}).strict(); - -export type SyntheticCleanupManifest = z.infer; +export const syntheticCleanupTableKeys = [ + "rooms", + "tasks", + "hunts", + "integrations", + "researchWatchlists", + "reportManifests", + "reportSchedules", + "messages", + "evidence", + "agentMemories", + "actors", +] as const; + +const SyntheticCleanupTableKeySchema = z.enum(syntheticCleanupTableKeys); +type SyntheticCleanupTableKey = z.infer; + +const ids = z + .array(z.uuid()) + .max(10_000) + .refine( + (value) => new Set(value).size === value.length, + "Candidate IDs must be unique", + ); +const digest = z.string().regex(/^[a-f0-9]{64}$/); +const tableDigests = z + .object({ + rooms: digest, + tasks: digest, + hunts: digest, + integrations: digest, + researchWatchlists: digest, + reportManifests: digest, + reportSchedules: digest, + messages: digest, + evidence: digest, + agentMemories: digest, + actors: digest, + }) + .strict(); +const selectionEvidence = z.object({ + table: SyntheticCleanupTableKeySchema, + recordId: z.uuid(), + provenanceId: z.uuid(), +}); +const objectStorageObject = z + .object({ + evidenceId: z.uuid(), + bucket: z.string().min(1).max(255), + key: z.string().min(1).max(2_048), + versionId: z + .string() + .min(1) + .max(2_048) + .refine( + (value) => value !== "unversioned" && value !== "null", + "Cleanup requires an immutable object version", + ), + etag: z.string().min(1).max(512), + size: z.number().int().nonnegative(), + sha256: digest, + legalHold: z.literal(false), + objectLockMetadata: z.record(z.string(), z.unknown()), + }) + .strict(); + +export const SyntheticCleanupManifestSchema = z + .object({ + version: z.literal(2), + manifestId: z.uuid(), + approvalId: z.uuid(), + organisationId: z.uuid(), + maintenanceActorId: z.uuid(), + generatedAt: z.string().datetime({ offset: true }), + digest, + archiveRoomIds: ids.default([]), + archiveTaskIds: ids.default([]), + archiveHuntIds: ids.default([]), + archiveIntegrationIds: ids.default([]), + archiveResearchWatchlistIds: ids.default([]), + archiveReportManifestIds: ids.default([]), + archiveReportScheduleIds: ids.default([]), + hideMessageIds: ids.default([]), + retireEvidenceIds: ids.default([]), + rejectAgentMemoryIds: ids.default([]), + retireActorIds: ids.default([]), + selectionEvidence: z.array(selectionEvidence).max(110_000), + objectStorageObjects: z.array(objectStorageObject).max(10_000).default([]), + tableDigests, + }) + .strict(); +export const SyntheticCleanupPlanSchema = SyntheticCleanupManifestSchema.omit({ + digest: true, + tableDigests: true, +}); +export const SyntheticCleanupObjectRetrySchema = z + .object({ + manifest: SyntheticCleanupManifestSchema, + retryApprovalId: z.uuid(), + }) + .strict(); + +export type SyntheticCleanupManifest = z.infer< + typeof SyntheticCleanupManifestSchema +>; +export type SyntheticCleanupPlan = z.infer; +export type SyntheticCleanupObject = z.infer; +export type SyntheticCleanupObjectRetry = z.infer< + typeof SyntheticCleanupObjectRetrySchema +>; + +type Database = ReturnType; +type Transaction = Parameters[0]>[0]; +type CandidateRow = { id: string; [key: string]: unknown }; +type CandidateRows = Record; function canonical(value: unknown): string { + if (value instanceof Date) return JSON.stringify(value.toISOString()); if (value === null || typeof value !== "object") return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; return `{${Object.entries(value as Record) @@ -46,18 +147,71 @@ function canonical(value: unknown): string { .join(",")}}`; } +function sha256(value: string) { + return createHash("sha256").update(value).digest("hex"); +} + export function syntheticCleanupManifestDigest( manifest: Omit, ) { - return createHash("sha256").update(canonical(manifest)).digest("hex"); + return sha256(canonical(manifest)); +} + +export function syntheticCleanupTableDigest(rows: ReadonlyArray) { + return sha256( + canonical([...rows].sort((left, right) => left.id.localeCompare(right.id))), + ); +} + +function candidateIds( + manifest: SyntheticCleanupManifest, +): Record { + return { + rooms: manifest.archiveRoomIds, + tasks: manifest.archiveTaskIds, + hunts: manifest.archiveHuntIds, + integrations: manifest.archiveIntegrationIds, + researchWatchlists: manifest.archiveResearchWatchlistIds, + reportManifests: manifest.archiveReportManifestIds, + reportSchedules: manifest.archiveReportScheduleIds, + messages: manifest.hideMessageIds, + evidence: manifest.retireEvidenceIds, + agentMemories: manifest.rejectAgentMemoryIds, + actors: manifest.retireActorIds, + }; +} + +function verifySelectionEvidence(manifest: SyntheticCleanupManifest) { + const expected = new Set( + Object.entries(candidateIds(manifest)).flatMap(([table, recordIds]) => + recordIds.map((recordId) => `${table}:${recordId}`), + ), + ); + const actual = new Set( + manifest.selectionEvidence.map((item) => `${item.table}:${item.recordId}`), + ); + if ( + actual.size !== manifest.selectionEvidence.length || + actual.size !== expected.size || + [...expected].some((key) => !actual.has(key)) + ) { + throw new Error( + "Cleanup selection evidence must exactly cover every candidate", + ); + } } export function parseSyntheticCleanupManifest(input: unknown) { const manifest = SyntheticCleanupManifestSchema.parse(input); - const { digest, ...unsigned } = manifest; - if (syntheticCleanupManifestDigest(unsigned) !== digest) { + const { digest: manifestDigest, ...unsigned } = manifest; + if (syntheticCleanupManifestDigest(unsigned) !== manifestDigest) { throw new Error("Cleanup manifest digest mismatch"); } + validateManifestGuards(manifest); + return manifest; +} + +function validateManifestGuards(manifest: SyntheticCleanupManifest) { if ( manifest.hideMessageIds.some((id) => (protectedDirectMessageIds as readonly string[]).includes(id), @@ -65,100 +219,1776 @@ export function parseSyntheticCleanupManifest(input: unknown) { ) { throw new Error("Cleanup manifest includes protected direct message"); } - return manifest; + if (manifest.retireActorIds.includes(manifest.maintenanceActorId)) { + throw new Error("Cleanup manifest retires its maintenance actor"); + } + verifySelectionEvidence(manifest); + const retiredEvidence = new Set(manifest.retireEvidenceIds); + if ( + new Set(manifest.objectStorageObjects.map((object) => object.evidenceId)) + .size !== manifest.objectStorageObjects.length || + manifest.objectStorageObjects.length !== retiredEvidence.size || + manifest.objectStorageObjects.some( + (object) => !retiredEvidence.has(object.evidenceId), + ) + ) { + throw new Error( + "Object cleanup inventory must exactly cover selected evidence", + ); + } } -/** - * Applies only reversible/governed transitions. Candidate selection and any - * physical object disposition stay outside this command; audit/outbox/message - * history remain immutable. - */ -export async function applySyntheticCleanup( - input: unknown, - traceId: string, - db = database(), +function validateAuthenticatedSubject( + subject: AuthorisationSubject, + manifest: SyntheticCleanupManifest, ) { - const manifest = parseSyntheticCleanupManifest(input); - return db.transaction(async (tx) => { - const idempotencyKey = `maintenance.synthetic-cleanup:${manifest.manifestId}`; - const [prior] = await tx - .select({ id: schema.outboxEvents.id }) - .from(schema.outboxEvents) - .where(eq(schema.outboxEvents.idempotencyKey, idempotencyKey)) - .limit(1) - .for("update"); - if (prior) return { applied: false, manifestId: manifest.manifestId }; - - const now = new Date(); - if (manifest.archiveRoomIds.length) { - await tx.update(schema.rooms).set({ archivedAt: now, updatedAt: now }).where(and( - eq(schema.rooms.organisationId, manifest.organisationId), - inArray(schema.rooms.id, manifest.archiveRoomIds), - )); + requireCapability(subject, "administration.manage"); + if ( + subject.organisationId !== manifest.organisationId || + subject.actorId !== manifest.maintenanceActorId + ) { + throw new ForbiddenError("administration.manage"); + } +} + +function assertExactRows( + table: SyntheticCleanupTableKey, + expectedIds: readonly string[], + rows: readonly CandidateRow[], + expectedDigest: string, +) { + const actualIds = rows.map((row) => row.id).sort(); + const sortedExpected = [...expectedIds].sort(); + if ( + actualIds.length !== sortedExpected.length || + actualIds.some((id, index) => id !== sortedExpected[index]) + ) { + throw new Error(`Cleanup ${table} candidate count or ownership changed`); + } + if (syntheticCleanupTableDigest(rows) !== expectedDigest) { + throw new Error(`Cleanup ${table} pre-state digest changed`); + } +} + +async function loadCandidateRows( + tx: Transaction, + manifest: SyntheticCleanupManifest, +): Promise { + const organisationId = manifest.organisationId; + const rooms = manifest.archiveRoomIds.length + ? await tx + .select({ ...getTableColumns(schema.rooms) }) + .from(schema.rooms) + .where( + and( + eq(schema.rooms.organisationId, organisationId), + inArray(schema.rooms.id, manifest.archiveRoomIds), + ), + ) + .for("update") + : []; + const tasks = manifest.archiveTaskIds.length + ? await tx + .select({ ...getTableColumns(schema.tasks) }) + .from(schema.tasks) + .where( + and( + eq(schema.tasks.organisationId, organisationId), + inArray(schema.tasks.id, manifest.archiveTaskIds), + ), + ) + .for("update") + : []; + const hunts = manifest.archiveHuntIds.length + ? await tx + .select({ ...getTableColumns(schema.huntRuns) }) + .from(schema.huntRuns) + .where( + and( + eq(schema.huntRuns.organisationId, organisationId), + inArray(schema.huntRuns.id, manifest.archiveHuntIds), + ), + ) + .for("update") + : []; + const integrations = manifest.archiveIntegrationIds.length + ? await tx + .select({ ...getTableColumns(schema.integrationRecords) }) + .from(schema.integrationRecords) + .where( + and( + eq(schema.integrationRecords.organisationId, organisationId), + inArray( + schema.integrationRecords.id, + manifest.archiveIntegrationIds, + ), + ), + ) + .for("update") + : []; + const researchWatchlists = manifest.archiveResearchWatchlistIds.length + ? await tx + .select({ ...getTableColumns(schema.researchWatchlists) }) + .from(schema.researchWatchlists) + .where( + and( + eq(schema.researchWatchlists.organisationId, organisationId), + inArray( + schema.researchWatchlists.id, + manifest.archiveResearchWatchlistIds, + ), + ), + ) + .for("update") + : []; + const reportManifests = manifest.archiveReportManifestIds.length + ? await tx + .select({ ...getTableColumns(schema.reportManifests) }) + .from(schema.reportManifests) + .where( + and( + eq(schema.reportManifests.organisationId, organisationId), + inArray( + schema.reportManifests.id, + manifest.archiveReportManifestIds, + ), + ), + ) + .for("update") + : []; + const reportSchedules = manifest.archiveReportScheduleIds.length + ? await tx + .select({ ...getTableColumns(schema.reportSchedules) }) + .from(schema.reportSchedules) + .where( + and( + eq(schema.reportSchedules.organisationId, organisationId), + inArray( + schema.reportSchedules.id, + manifest.archiveReportScheduleIds, + ), + ), + ) + .for("update") + : []; + const messages = manifest.hideMessageIds.length + ? await tx + .select({ + ...getTableColumns(schema.messages), + roomType: schema.rooms.roomType, + }) + .from(schema.messages) + .innerJoin( + schema.rooms, + and( + eq(schema.rooms.organisationId, schema.messages.organisationId), + eq(schema.rooms.id, schema.messages.roomId), + ), + ) + .where( + and( + eq(schema.messages.organisationId, organisationId), + inArray(schema.messages.id, manifest.hideMessageIds), + ), + ) + .for("update", { of: [schema.messages, schema.rooms] }) + : []; + const evidence = manifest.retireEvidenceIds.length + ? await tx + .select({ ...getTableColumns(schema.evidence) }) + .from(schema.evidence) + .where( + and( + eq(schema.evidence.organisationId, organisationId), + inArray(schema.evidence.id, manifest.retireEvidenceIds), + ), + ) + .for("update") + : []; + const agentMemories = manifest.rejectAgentMemoryIds.length + ? await tx + .select({ ...getTableColumns(schema.agentMemories) }) + .from(schema.agentMemories) + .where( + and( + eq(schema.agentMemories.organisationId, organisationId), + inArray(schema.agentMemories.id, manifest.rejectAgentMemoryIds), + ), + ) + .for("update") + : []; + const actors = manifest.retireActorIds.length + ? await tx + .select({ ...getTableColumns(schema.actors) }) + .from(schema.actors) + .where( + and( + eq(schema.actors.organisationId, organisationId), + inArray(schema.actors.id, manifest.retireActorIds), + ), + ) + .for("update") + : []; + const rows: CandidateRows = { + rooms, + tasks, + hunts, + integrations, + researchWatchlists, + reportManifests, + reportSchedules, + messages, + evidence, + agentMemories, + actors, + }; + const selectedArtifactIds = Object.values(candidateIds(manifest)).flat(); + const provenance = selectedArtifactIds.length + ? await tx + .select({ + id: schema.syntheticArtifactProvenance.id, + artifactTable: schema.syntheticArtifactProvenance.artifactTable, + artifactId: schema.syntheticArtifactProvenance.artifactId, + sourceKind: schema.syntheticArtifactProvenance.sourceKind, + sourceReference: schema.syntheticArtifactProvenance.sourceReference, + }) + .from(schema.syntheticArtifactProvenance) + .where( + and( + eq( + schema.syntheticArtifactProvenance.organisationId, + organisationId, + ), + inArray( + schema.syntheticArtifactProvenance.artifactId, + selectedArtifactIds, + ), + ), + ) + .for("update") + : []; + const provenanceByArtifact = new Map( + provenance.map((record) => [ + `${record.artifactTable}:${record.artifactId}`, + record, + ]), + ); + for (const table of syntheticCleanupTableKeys) { + for (const row of rows[table]) { + const record = provenanceByArtifact.get(`${table}:${row.id}`); + if (record) { + row.syntheticProvenanceId = record.id; + row.syntheticProvenanceSourceKind = record.sourceKind; + row.syntheticProvenanceSourceReference = record.sourceReference; + } } - for (const messageId of manifest.hideMessageIds) { - const [message] = await tx.select().from(schema.messages).where(and( - eq(schema.messages.organisationId, manifest.organisationId), - eq(schema.messages.id, messageId), - )).limit(1).for("update"); - if (!message || message.deletedAt) continue; - await tx.insert(schema.messageRevisions).values({ - id: newId(), organisationId: manifest.organisationId, messageId, - actorId: manifest.maintenanceActorId, revisionType: "delete", - previousDocument: message.document, previousPlainText: message.plainText, - nextDocument: null, nextPlainText: null, - reason: `Synthetic cleanup manifest ${manifest.manifestId}`, - idempotencyKey: `${idempotencyKey}:message:${messageId}`, - }); - await tx.update(schema.messages).set({ - document: { type: "doc", content: [] }, plainText: "Message deleted", deletedAt: now, - }).where(and(eq(schema.messages.organisationId, manifest.organisationId), eq(schema.messages.id, messageId))); + } + if (manifest.retireActorIds.length) { + const protectedMemberships = await tx + .select({ actorId: schema.roomMemberships.actorId }) + .from(schema.roomMemberships) + .innerJoin( + schema.rooms, + and( + eq( + schema.rooms.organisationId, + schema.roomMemberships.organisationId, + ), + eq(schema.rooms.id, schema.roomMemberships.roomId), + ), + ) + .where( + and( + eq(schema.roomMemberships.organisationId, organisationId), + inArray(schema.roomMemberships.actorId, manifest.retireActorIds), + eq(schema.rooms.roomType, "direct"), + ), + ) + .for("update", { of: [schema.roomMemberships, schema.rooms] }); + if (protectedMemberships.length) { + throw new Error("Cleanup cannot retire a direct-room member"); } - if (manifest.retireEvidenceIds.length) { - await tx.update(schema.evidence).set({ retentionState: "retired" }).where(and( - eq(schema.evidence.organisationId, manifest.organisationId), - inArray(schema.evidence.id, manifest.retireEvidenceIds), - eq(schema.evidence.legalHold, false), - )); + } + return rows; +} + +function verifyCandidateRows( + manifest: SyntheticCleanupManifest, + rows: CandidateRows, +) { + for (const table of syntheticCleanupTableKeys) { + assertExactRows( + table, + candidateIds(manifest)[table], + rows[table], + manifest.tableDigests[table], + ); + } + verifyCandidateStates(manifest, rows); +} + +function verifyCandidateStates( + manifest: SyntheticCleanupManifest, + rows: CandidateRows, +) { + if ( + rows.rooms.some( + (row) => row.archivedAt !== null || row.roomType === "direct", + ) + ) { + throw new Error("Cleanup room is archived or direct"); + } + if ( + rows.tasks.some((row) => row.archivedAt !== null || row.status !== "done") + ) { + throw new Error("Cleanup tasks must be done and unarchived"); + } + if ( + rows.hunts.some( + (row) => + row.archivedAt !== null || + !["completed", "failed", "cancelled"].includes(String(row.status)), + ) + ) { + throw new Error("Cleanup hunts must be terminal and unarchived"); + } + if (rows.integrations.some((row) => row.archivedAt !== null)) { + throw new Error("Cleanup integration is already archived"); + } + if (rows.researchWatchlists.some((row) => row.archivedAt !== null)) { + throw new Error("Cleanup research watchlist is already archived"); + } + if (rows.reportManifests.some((row) => row.archivedAt !== null)) { + throw new Error("Cleanup report manifest is already archived"); + } + if (rows.reportSchedules.some((row) => row.archivedAt !== null)) { + throw new Error("Cleanup report schedule is already archived"); + } + if ( + rows.messages.some( + (row) => row.deletedAt !== null || row.roomType === "direct", + ) + ) { + throw new Error("Cleanup cannot hide deleted or direct-room messages"); + } + if ( + rows.evidence.some( + (row) => row.legalHold === true || row.retentionState === "retired", + ) + ) { + throw new Error("Cleanup evidence is held or already retired"); + } + if (rows.agentMemories.some((row) => row.status === "rejected")) { + throw new Error("Cleanup memory is already rejected"); + } + if ( + rows.actors.some( + (row) => + row.status !== "active" || + row.actorType !== "human" || + row.id === manifest.maintenanceActorId, + ) + ) { + throw new Error("Cleanup actor is protected or already inactive"); + } + const expectedObjects = new Map( + manifest.objectStorageObjects.map((object) => [object.evidenceId, object]), + ); + for (const row of rows.evidence) { + const object = expectedObjects.get(row.id); + if (!object) continue; + if ( + object.key !== row.storageKey || + object.sha256 !== row.sha256 || + object.size !== row.size + ) { + throw new Error("Cleanup object inventory changed"); } - if (manifest.rejectAgentMemoryIds.length) { - await tx.update(schema.agentMemories).set({ status: "rejected", expiresAt: now }).where(and( - eq(schema.agentMemories.organisationId, manifest.organisationId), - inArray(schema.agentMemories.id, manifest.rejectAgentMemoryIds), - )); + if ( + object.legalHold || + Object.keys(object.objectLockMetadata).length > 0 || + (row.objectLockMetadata && + Object.keys(row.objectLockMetadata as object).length > 0) + ) { + throw new Error("Cleanup object is locked"); } - if (manifest.disableWatchlistIds.length) { - await tx.update(schema.researchWatchlists).set({ enabled: false, updatedAt: now }).where(and( - eq(schema.researchWatchlists.organisationId, manifest.organisationId), - inArray(schema.researchWatchlists.id, manifest.disableWatchlistIds), - )); + } + const rowsByTable = Object.fromEntries( + syntheticCleanupTableKeys.map((table) => [ + table, + new Map(rows[table].map((row) => [row.id, row])), + ]), + ) as Record>; + for (const evidence of manifest.selectionEvidence) { + const row = rowsByTable[evidence.table].get(evidence.recordId); + if (!row) { + throw new Error("Cleanup selection evidence row is unavailable"); } - await appendAuditEvent(tx, { - organisationId: manifest.organisationId, actorId: manifest.maintenanceActorId, - actorType: "human", action: "maintenance.synthetic_cleanup.applied", - targetType: "cleanup_manifest", targetId: manifest.manifestId, - metadata: { digest: manifest.digest, tableDigests: manifest.tableDigests }, traceId, - }); - await writeOutbox(tx, { - organisationId: manifest.organisationId, eventType: "maintenance.synthetic_cleanup.applied", - aggregateType: "cleanup_manifest", aggregateId: manifest.manifestId, - queueName: "muster-outbox", payload: { manifestId: manifest.manifestId, digest: manifest.digest }, - idempotencyKey, traceId, + if (row.syntheticProvenanceId !== evidence.provenanceId) { + throw new Error( + "Cleanup candidate lacks exact append-only synthetic provenance", + ); + } + } +} + +async function validateMaintenanceActor( + tx: Transaction, + manifest: SyntheticCleanupManifest, +) { + const [actor] = await tx + .select({ + id: schema.actors.id, + status: schema.actors.status, + actorType: schema.actors.actorType, + capabilities: schema.actors.capabilityAssignments, + }) + .from(schema.actors) + .where( + and( + eq(schema.actors.organisationId, manifest.organisationId), + eq(schema.actors.id, manifest.maintenanceActorId), + ), + ) + .limit(1) + .for("update"); + const capabilities = z.array(z.string()).safeParse(actor?.capabilities); + if ( + !actor || + actor.status !== "active" || + actor.actorType !== "human" || + !capabilities.success || + !capabilities.data.includes("administration.manage") + ) { + throw new Error("Authorised maintenance actor is unavailable"); + } + return actor; +} + +async function validateExecutableApproval( + tx: Transaction, + manifest: SyntheticCleanupManifest, + now: Date, +) { + const [approval] = await tx + .select() + .from(schema.approvals) + .where( + and( + eq(schema.approvals.organisationId, manifest.organisationId), + eq(schema.approvals.id, manifest.approvalId), + ), + ) + .limit(1) + .for("update"); + const target = z + .object({ manifestId: z.uuid(), digest }) + .safeParse(approval?.target); + const decisions = z + .array( + z.object({ + actorId: z.uuid(), + status: z.enum(["approved", "rejected"]), + }), + ) + .safeParse(approval?.decisions); + if ( + !approval || + approval.status !== "approved" || + approval.actionType !== "maintenance.synthetic-cleanup" || + approval.requiredCapability !== "administration.manage" || + approval.requiredApprovalCount !== + actionApprovalPolicy["maintenance.synthetic-cleanup"].approvalCount || + approval.requestingActorId !== manifest.maintenanceActorId || + approval.expiresAt <= now || + approval.executedAt !== null || + !target.success || + target.data.manifestId !== manifest.manifestId || + target.data.digest !== manifest.digest || + !decisions.success + ) { + throw new Error("Executable cleanup approval is missing"); + } + assertExecutableApproval("maintenance.synthetic-cleanup", decisions.data); + const approvedActorIds = [ + ...new Set( + decisions.data + .filter((decision) => decision.status === "approved") + .map((decision) => decision.actorId), + ), + ]; + if (approvedActorIds.length < approval.requiredApprovalCount) { + throw new Error("Cleanup approval count is insufficient"); + } + if (approvedActorIds.includes(manifest.maintenanceActorId)) { + throw new Error("Cleanup requires an independent approver"); + } + const approvers = await tx + .select({ + id: schema.actors.id, + status: schema.actors.status, + capabilities: schema.actors.capabilityAssignments, + }) + .from(schema.actors) + .where( + and( + eq(schema.actors.organisationId, manifest.organisationId), + inArray(schema.actors.id, approvedActorIds), + ), + ) + .for("update"); + if ( + approvers.length !== approvedActorIds.length || + approvers.some((approver) => { + const capabilities = z.array(z.string()).safeParse(approver.capabilities); + return ( + approver.status !== "active" || + !capabilities.success || + !capabilities.data.includes("administration.manage") + ); + }) + ) { + throw new Error("Cleanup approver capability was revoked"); + } + return approval; +} + +function candidateCounts(rows: CandidateRows) { + return Object.fromEntries( + syntheticCleanupTableKeys.map((table) => [table, rows[table].length]), + ) as Record; +} + +async function updateExactly( + expectedIds: readonly string[], + update: () => Promise>, + label: string, +) { + if (!expectedIds.length) return; + const updated = await update(); + if ( + updated.length !== expectedIds.length || + updated.some((row) => !expectedIds.includes(row.id)) + ) { + throw new Error(`Cleanup ${label} affected-count mismatch`); + } +} + +async function applyCandidateTransitions( + tx: Transaction, + manifest: SyntheticCleanupManifest, + now: Date, + idempotencyKey: string, +) { + await updateExactly( + manifest.archiveRoomIds, + () => + tx + .update(schema.rooms) + .set({ archivedAt: now, updatedAt: now }) + .where( + and( + eq(schema.rooms.organisationId, manifest.organisationId), + inArray(schema.rooms.id, manifest.archiveRoomIds), + ), + ) + .returning({ id: schema.rooms.id }), + "rooms", + ); + await updateExactly( + manifest.archiveTaskIds, + () => + tx + .update(schema.tasks) + .set({ archivedAt: now, updatedAt: now }) + .where( + and( + eq(schema.tasks.organisationId, manifest.organisationId), + inArray(schema.tasks.id, manifest.archiveTaskIds), + ), + ) + .returning({ id: schema.tasks.id }), + "tasks", + ); + await updateExactly( + manifest.archiveHuntIds, + () => + tx + .update(schema.huntRuns) + .set({ archivedAt: now, updatedAt: now }) + .where( + and( + eq(schema.huntRuns.organisationId, manifest.organisationId), + inArray(schema.huntRuns.id, manifest.archiveHuntIds), + ), + ) + .returning({ id: schema.huntRuns.id }), + "hunts", + ); + await updateExactly( + manifest.archiveIntegrationIds, + () => + tx + .update(schema.integrationRecords) + .set({ archivedAt: now, status: "disabled", updatedAt: now }) + .where( + and( + eq( + schema.integrationRecords.organisationId, + manifest.organisationId, + ), + inArray( + schema.integrationRecords.id, + manifest.archiveIntegrationIds, + ), + ), + ) + .returning({ id: schema.integrationRecords.id }), + "integrations", + ); + await updateExactly( + manifest.archiveResearchWatchlistIds, + () => + tx + .update(schema.researchWatchlists) + .set({ archivedAt: now, enabled: false, updatedAt: now }) + .where( + and( + eq( + schema.researchWatchlists.organisationId, + manifest.organisationId, + ), + inArray( + schema.researchWatchlists.id, + manifest.archiveResearchWatchlistIds, + ), + ), + ) + .returning({ id: schema.researchWatchlists.id }), + "research watchlists", + ); + await updateExactly( + manifest.archiveReportManifestIds, + () => + tx + .update(schema.reportManifests) + .set({ archivedAt: now, updatedAt: now }) + .where( + and( + eq(schema.reportManifests.organisationId, manifest.organisationId), + inArray( + schema.reportManifests.id, + manifest.archiveReportManifestIds, + ), + ), + ) + .returning({ id: schema.reportManifests.id }), + "report manifests", + ); + await updateExactly( + manifest.archiveReportScheduleIds, + () => + tx + .update(schema.reportSchedules) + .set({ archivedAt: now, enabled: false, updatedAt: now }) + .where( + and( + eq(schema.reportSchedules.organisationId, manifest.organisationId), + inArray( + schema.reportSchedules.id, + manifest.archiveReportScheduleIds, + ), + ), + ) + .returning({ id: schema.reportSchedules.id }), + "report schedules", + ); + for (const messageId of manifest.hideMessageIds) { + const [message] = await tx + .select() + .from(schema.messages) + .where( + and( + eq(schema.messages.organisationId, manifest.organisationId), + eq(schema.messages.id, messageId), + ), + ) + .limit(1); + if (!message || message.deletedAt) { + throw new Error("Cleanup message changed after preflight"); + } + await tx.insert(schema.messageRevisions).values({ + id: newId(), + organisationId: manifest.organisationId, + messageId, + actorId: manifest.maintenanceActorId, + revisionType: "delete", + previousDocument: message.document, + previousPlainText: message.plainText, + nextDocument: null, + nextPlainText: null, + reason: `Synthetic cleanup manifest ${manifest.manifestId}`, + idempotencyKey: `${idempotencyKey}:message:${messageId}`, }); - return { applied: true, manifestId: manifest.manifestId }; - }, { isolationLevel: "serializable", accessMode: "read write" }); + const updated = await tx + .update(schema.messages) + .set({ + document: { type: "doc", content: [] }, + plainText: "Message deleted", + deletedAt: now, + }) + .where( + and( + eq(schema.messages.organisationId, manifest.organisationId), + eq(schema.messages.id, messageId), + ), + ) + .returning({ id: schema.messages.id }); + if (updated.length !== 1) { + throw new Error("Cleanup message affected-count mismatch"); + } + } + await updateExactly( + manifest.retireEvidenceIds, + () => + tx + .update(schema.evidence) + .set({ retentionState: "retired" }) + .where( + and( + eq(schema.evidence.organisationId, manifest.organisationId), + inArray(schema.evidence.id, manifest.retireEvidenceIds), + eq(schema.evidence.legalHold, false), + ), + ) + .returning({ id: schema.evidence.id }), + "evidence", + ); + await updateExactly( + manifest.rejectAgentMemoryIds, + () => + tx + .update(schema.agentMemories) + .set({ status: "rejected", expiresAt: now }) + .where( + and( + eq(schema.agentMemories.organisationId, manifest.organisationId), + inArray(schema.agentMemories.id, manifest.rejectAgentMemoryIds), + ), + ) + .returning({ id: schema.agentMemories.id }), + "agent memories", + ); + await updateExactly( + manifest.retireActorIds, + () => + tx + .update(schema.actors) + .set({ status: "inactive" }) + .where( + and( + eq(schema.actors.organisationId, manifest.organisationId), + inArray(schema.actors.id, manifest.retireActorIds), + ), + ) + .returning({ id: schema.actors.id }), + "actors", + ); +} + +function verifyPostCandidateStates( + manifest: SyntheticCleanupManifest, + before: CandidateRows, + after: CandidateRows, + now: Date, +) { + const expected: CandidateRows = Object.fromEntries( + syntheticCleanupTableKeys.map((table) => [ + table, + before[table].map((row) => ({ ...row })), + ]), + ) as CandidateRows; + for (const row of expected.rooms) { + row.archivedAt = now; + row.updatedAt = now; + } + for (const row of expected.tasks) { + row.archivedAt = now; + row.updatedAt = now; + } + for (const row of expected.hunts) { + row.archivedAt = now; + row.updatedAt = now; + } + for (const row of expected.integrations) { + row.archivedAt = now; + row.status = "disabled"; + row.updatedAt = now; + } + for (const row of expected.researchWatchlists) { + row.archivedAt = now; + row.enabled = false; + row.updatedAt = now; + } + for (const row of expected.reportManifests) { + row.archivedAt = now; + row.updatedAt = now; + } + for (const row of expected.reportSchedules) { + row.archivedAt = now; + row.enabled = false; + row.updatedAt = now; + } + for (const row of expected.messages) { + row.document = { type: "doc", content: [] }; + row.plainText = "Message deleted"; + row.deletedAt = now; + } + for (const row of expected.evidence) { + row.retentionState = "retired"; + } + for (const row of expected.agentMemories) { + row.status = "rejected"; + row.expiresAt = now; + } + for (const row of expected.actors) { + row.status = "inactive"; + } + for (const table of syntheticCleanupTableKeys) { + assertExactRows( + table, + candidateIds(manifest)[table], + after[table], + syntheticCleanupTableDigest(expected[table]), + ); + } + return Object.fromEntries( + syntheticCleanupTableKeys.map((table) => [ + table, + syntheticCleanupTableDigest(expected[table]), + ]), + ) as Record; +} + +async function verifyMessageRevisions( + tx: Transaction, + manifest: SyntheticCleanupManifest, + before: CandidateRows, + idempotencyKey: string, +) { + if (!manifest.hideMessageIds.length) return; + const revisionKeys = manifest.hideMessageIds.map( + (messageId) => `${idempotencyKey}:message:${messageId}`, + ); + const revisions = await tx + .select() + .from(schema.messageRevisions) + .where( + and( + eq(schema.messageRevisions.organisationId, manifest.organisationId), + inArray(schema.messageRevisions.idempotencyKey, revisionKeys), + ), + ); + const beforeById = new Map( + before.messages.map((message) => [message.id, message]), + ); + if ( + revisions.length !== manifest.hideMessageIds.length || + revisions.some((revision) => { + const previous = beforeById.get(revision.messageId); + return ( + !previous || + revision.actorId !== manifest.maintenanceActorId || + revision.revisionType !== "delete" || + canonical(revision.previousDocument) !== canonical(previous.document) || + revision.previousPlainText !== previous.plainText || + revision.nextDocument !== null || + revision.nextPlainText !== null || + revision.idempotencyKey !== + `${idempotencyKey}:message:${revision.messageId}` + ); + }) + ) { + throw new Error("Cleanup message revision post-state mismatch"); + } +} + +export async function requestSyntheticCleanupApproval( + subject: AuthorisationSubject, + input: unknown, + traceId: string, + db = database(), +) { + const manifest = parseSyntheticCleanupManifest(input); + validateAuthenticatedSubject(subject, manifest); + return db.transaction( + async (tx) => { + await validateMaintenanceActor(tx, manifest); + const rows = await loadCandidateRows(tx, manifest); + verifyCandidateRows(manifest, rows); + const [prior] = await tx + .select({ + id: schema.approvals.id, + actionType: schema.approvals.actionType, + target: schema.approvals.target, + }) + .from(schema.approvals) + .where( + and( + eq(schema.approvals.organisationId, manifest.organisationId), + eq(schema.approvals.id, manifest.approvalId), + ), + ) + .limit(1) + .for("update"); + if (prior) { + const target = z + .object({ manifestId: z.uuid(), digest }) + .safeParse(prior.target); + if ( + prior.actionType !== "maintenance.synthetic-cleanup" || + !target.success || + target.data.manifestId !== manifest.manifestId || + target.data.digest !== manifest.digest + ) { + throw new Error("Cleanup approval ID is already bound"); + } + return { + requested: false, + manifestId: manifest.manifestId, + approvalId: manifest.approvalId, + candidateCounts: candidateCounts(rows), + }; + } + const policy = actionApprovalPolicy["maintenance.synthetic-cleanup"]; + await tx.insert(schema.approvals).values({ + id: manifest.approvalId, + organisationId: manifest.organisationId, + requestingActorId: manifest.maintenanceActorId, + actionType: "maintenance.synthetic-cleanup", + target: { + manifestId: manifest.manifestId, + digest: manifest.digest, + }, + riskSummary: `Archive or retire ${Object.values(candidateCounts(rows)).reduce((sum, count) => sum + count, 0)} exact synthetic records; object deletion remains post-commit.`, + expiresAt: new Date(Date.now() + 30 * 60_000), + requiredCapability: policy.capability, + requiredApprovalCount: policy.approvalCount, + idempotencyKey: `maintenance.synthetic-cleanup:${manifest.manifestId}:approval`, + }); + await appendAuditEvent(tx, { + organisationId: manifest.organisationId, + actorId: manifest.maintenanceActorId, + actorType: "human", + action: "maintenance.synthetic_cleanup.requested", + targetType: "cleanup_manifest", + targetId: manifest.manifestId, + metadata: { + approvalId: manifest.approvalId, + digest: manifest.digest, + candidateCounts: candidateCounts(rows), + tableDigests: manifest.tableDigests, + }, + traceId, + }); + await writeOutbox(tx, { + organisationId: manifest.organisationId, + eventType: "maintenance.synthetic_cleanup.requested", + aggregateType: "cleanup_manifest", + aggregateId: manifest.manifestId, + queueName: "muster-outbox", + payload: { + manifestId: manifest.manifestId, + approvalId: manifest.approvalId, + digest: manifest.digest, + }, + idempotencyKey: `maintenance.synthetic-cleanup:${manifest.manifestId}:requested`, + traceId, + }); + return { + requested: true, + manifestId: manifest.manifestId, + approvalId: manifest.approvalId, + candidateCounts: candidateCounts(rows), + }; + }, + { isolationLevel: "serializable", accessMode: "read write" }, + ); +} + +export async function captureSyntheticCleanupManifest( + subject: AuthorisationSubject, + input: unknown, + db = database(), +) { + const plan = SyntheticCleanupPlanSchema.parse(input); + const placeholder = { + ...plan, + digest: "0".repeat(64), + tableDigests: Object.fromEntries( + syntheticCleanupTableKeys.map((table) => [table, "0".repeat(64)]), + ), + } as SyntheticCleanupManifest; + validateManifestGuards(placeholder); + validateAuthenticatedSubject(subject, placeholder); + return db.transaction( + async (tx) => { + await validateMaintenanceActor(tx, placeholder); + const rows = await loadCandidateRows(tx, placeholder); + for (const table of syntheticCleanupTableKeys) { + const actualDigest = syntheticCleanupTableDigest(rows[table]); + assertExactRows( + table, + candidateIds(placeholder)[table], + rows[table], + actualDigest, + ); + } + verifyCandidateStates(placeholder, rows); + const capturedTableDigests = Object.fromEntries( + syntheticCleanupTableKeys.map((table) => [ + table, + syntheticCleanupTableDigest(rows[table]), + ]), + ) as SyntheticCleanupManifest["tableDigests"]; + const unsigned: Omit = { + ...plan, + tableDigests: capturedTableDigests, + }; + return { + ...unsigned, + digest: syntheticCleanupManifestDigest(unsigned), + }; + }, + { isolationLevel: "repeatable read", accessMode: "read write" }, + ); +} + +export async function verifySyntheticCleanup( + subject: AuthorisationSubject, + input: unknown, + db = database(), +) { + const manifest = parseSyntheticCleanupManifest(input); + validateAuthenticatedSubject(subject, manifest); + return db.transaction( + async (tx) => { + await validateMaintenanceActor(tx, manifest); + const rows = await loadCandidateRows(tx, manifest); + verifyCandidateRows(manifest, rows); + return { + verified: true, + manifestId: manifest.manifestId, + candidateCounts: candidateCounts(rows), + tableDigests: manifest.tableDigests, + }; + }, + { isolationLevel: "repeatable read", accessMode: "read write" }, + ); } -async function main() { - const [mode, manifestPath] = process.argv.slice(2); - if (mode !== "--apply" || !manifestPath?.startsWith("/")) { - throw new Error("Usage: cleanup:synthetic --apply /absolute/manifest.json"); +export async function findSyntheticCleanupReceipt( + subject: AuthorisationSubject, + input: unknown, + db = database(), +) { + const manifest = parseSyntheticCleanupManifest(input); + validateAuthenticatedSubject(subject, manifest); + const [receipt] = await db + .select() + .from(schema.syntheticCleanupReceipts) + .where( + and( + eq( + schema.syntheticCleanupReceipts.organisationId, + manifest.organisationId, + ), + eq(schema.syntheticCleanupReceipts.manifestId, manifest.manifestId), + ), + ) + .limit(1); + if (receipt && receipt.manifestDigest !== manifest.digest) { + throw new Error("Completed cleanup manifest digest changed"); + } + return receipt ?? null; +} + +export async function listSyntheticCleanupObjectDeletionAttempts( + subject: AuthorisationSubject, + input: unknown, + db = database(), +) { + const manifest = parseSyntheticCleanupManifest(input); + validateAuthenticatedSubject(subject, manifest); + return db + .select() + .from(schema.syntheticCleanupObjectDeletionAttempts) + .where( + and( + eq( + schema.syntheticCleanupObjectDeletionAttempts.organisationId, + manifest.organisationId, + ), + eq( + schema.syntheticCleanupObjectDeletionAttempts.manifestId, + manifest.manifestId, + ), + ), + ); +} + +function pendingObjectVersions( + manifest: SyntheticCleanupManifest, + attempts: ReadonlyArray<{ + evidenceId: string; + versionId: string; + result: string; + }>, +) { + const completed = new Set( + attempts + .filter( + (attempt) => + attempt.result === "succeeded" || + attempt.result === "observed_missing", + ) + .map((attempt) => `${attempt.evidenceId}:${attempt.versionId}`), + ); + return manifest.objectStorageObjects.filter( + (object) => !completed.has(`${object.evidenceId}:${object.versionId}`), + ); +} + +function objectInventoryDigest(objects: readonly SyntheticCleanupObject[]) { + return sha256( + canonical( + [...objects].sort((left, right) => + left.evidenceId.localeCompare(right.evidenceId), + ), + ), + ); +} + +export async function requestSyntheticCleanupObjectRetryApproval( + subject: AuthorisationSubject, + input: unknown, + traceId: string, + db = database(), +) { + const retry = SyntheticCleanupObjectRetrySchema.parse(input); + const manifest = parseSyntheticCleanupManifest(retry.manifest); + validateAuthenticatedSubject(subject, manifest); + return db.transaction( + async (tx) => { + await validateMaintenanceActor(tx, manifest); + const [receipt] = await tx + .select({ digest: schema.syntheticCleanupReceipts.manifestDigest }) + .from(schema.syntheticCleanupReceipts) + .where( + and( + eq( + schema.syntheticCleanupReceipts.organisationId, + manifest.organisationId, + ), + eq(schema.syntheticCleanupReceipts.manifestId, manifest.manifestId), + ), + ) + .limit(1) + .for("update"); + if (!receipt || receipt.digest !== manifest.digest) { + throw new Error("Cleanup receipt is unavailable for retry"); + } + const attempts = await tx + .select({ + evidenceId: schema.syntheticCleanupObjectDeletionAttempts.evidenceId, + versionId: schema.syntheticCleanupObjectDeletionAttempts.versionId, + result: schema.syntheticCleanupObjectDeletionAttempts.result, + }) + .from(schema.syntheticCleanupObjectDeletionAttempts) + .where( + and( + eq( + schema.syntheticCleanupObjectDeletionAttempts.organisationId, + manifest.organisationId, + ), + eq( + schema.syntheticCleanupObjectDeletionAttempts.manifestId, + manifest.manifestId, + ), + ), + ) + .for("update"); + const pending = pendingObjectVersions(manifest, attempts); + if (!pending.length) { + throw new Error("Cleanup has no pending object versions"); + } + const pendingDigest = objectInventoryDigest(pending); + const [prior] = await tx + .select({ + actionType: schema.approvals.actionType, + target: schema.approvals.target, + }) + .from(schema.approvals) + .where( + and( + eq(schema.approvals.organisationId, manifest.organisationId), + eq(schema.approvals.id, retry.retryApprovalId), + ), + ) + .limit(1) + .for("update"); + if (prior) { + const target = z + .object({ + manifestId: z.uuid(), + manifestDigest: digest, + pendingObjectDigest: digest, + }) + .safeParse(prior.target); + if ( + prior.actionType !== + "maintenance.synthetic-cleanup.object-delete-retry" || + !target.success || + target.data.manifestId !== manifest.manifestId || + target.data.manifestDigest !== manifest.digest || + target.data.pendingObjectDigest !== pendingDigest + ) { + throw new Error("Cleanup object retry approval ID is already bound"); + } + return { + requested: false, + retryApprovalId: retry.retryApprovalId, + pendingObjectVersions: pending.length, + pendingObjectDigest: pendingDigest, + }; + } + const action = "maintenance.synthetic-cleanup.object-delete-retry"; + const policy = actionApprovalPolicy[action]; + await tx.insert(schema.approvals).values({ + id: retry.retryApprovalId, + organisationId: manifest.organisationId, + requestingActorId: manifest.maintenanceActorId, + actionType: action, + target: { + manifestId: manifest.manifestId, + manifestDigest: manifest.digest, + pendingObjectDigest: pendingDigest, + }, + riskSummary: `Retry or reconcile ${pending.length} exact immutable cleanup object versions.`, + expiresAt: new Date(Date.now() + 30 * 60_000), + requiredCapability: policy.capability, + requiredApprovalCount: policy.approvalCount, + idempotencyKey: `maintenance.synthetic-cleanup:${manifest.manifestId}:object-retry:${retry.retryApprovalId}`, + }); + await appendAuditEvent(tx, { + organisationId: manifest.organisationId, + actorId: manifest.maintenanceActorId, + actorType: "human", + action: "maintenance.synthetic_cleanup.object_retry_requested", + targetType: "cleanup_manifest", + targetId: manifest.manifestId, + metadata: { + retryApprovalId: retry.retryApprovalId, + pendingObjectDigest: pendingDigest, + pendingObjectVersions: pending.length, + }, + traceId, + }); + await writeOutbox(tx, { + organisationId: manifest.organisationId, + eventType: "maintenance.synthetic_cleanup.object_retry_requested", + aggregateType: "cleanup_manifest", + aggregateId: manifest.manifestId, + queueName: "muster-outbox", + payload: { + manifestId: manifest.manifestId, + retryApprovalId: retry.retryApprovalId, + pendingObjectDigest: pendingDigest, + }, + idempotencyKey: `maintenance.synthetic-cleanup:${manifest.manifestId}:object-retry:${retry.retryApprovalId}:requested`, + traceId, + }); + return { + requested: true, + retryApprovalId: retry.retryApprovalId, + pendingObjectVersions: pending.length, + pendingObjectDigest: pendingDigest, + }; + }, + { isolationLevel: "serializable", accessMode: "read write" }, + ); +} + +export async function authoriseSyntheticCleanupObjectRetry( + subject: AuthorisationSubject, + input: unknown, + traceId: string, + db = database(), +) { + const retry = SyntheticCleanupObjectRetrySchema.parse(input); + const manifest = parseSyntheticCleanupManifest(retry.manifest); + validateAuthenticatedSubject(subject, manifest); + return db.transaction( + async (tx) => { + await tx.execute( + sql`select pg_advisory_xact_lock(hashtextextended(${retry.retryApprovalId}, 0))`, + ); + await validateMaintenanceActor(tx, manifest); + const [receipt] = await tx + .select({ digest: schema.syntheticCleanupReceipts.manifestDigest }) + .from(schema.syntheticCleanupReceipts) + .where( + and( + eq( + schema.syntheticCleanupReceipts.organisationId, + manifest.organisationId, + ), + eq(schema.syntheticCleanupReceipts.manifestId, manifest.manifestId), + ), + ) + .limit(1) + .for("update"); + if (!receipt || receipt.digest !== manifest.digest) { + throw new Error("Cleanup receipt is unavailable for retry"); + } + const attempts = await tx + .select({ + evidenceId: schema.syntheticCleanupObjectDeletionAttempts.evidenceId, + versionId: schema.syntheticCleanupObjectDeletionAttempts.versionId, + result: schema.syntheticCleanupObjectDeletionAttempts.result, + }) + .from(schema.syntheticCleanupObjectDeletionAttempts) + .where( + and( + eq( + schema.syntheticCleanupObjectDeletionAttempts.organisationId, + manifest.organisationId, + ), + eq( + schema.syntheticCleanupObjectDeletionAttempts.manifestId, + manifest.manifestId, + ), + ), + ) + .for("update"); + const pending = pendingObjectVersions(manifest, attempts); + if (!pending.length) { + throw new Error("Cleanup has no pending object versions"); + } + const pendingDigest = objectInventoryDigest(pending); + const [approval] = await tx + .select() + .from(schema.approvals) + .where( + and( + eq(schema.approvals.organisationId, manifest.organisationId), + eq(schema.approvals.id, retry.retryApprovalId), + ), + ) + .limit(1) + .for("update"); + const target = z + .object({ + manifestId: z.uuid(), + manifestDigest: digest, + pendingObjectDigest: digest, + }) + .safeParse(approval?.target); + const decisions = z + .array( + z.object({ + actorId: z.uuid(), + status: z.enum(["approved", "rejected"]), + }), + ) + .safeParse(approval?.decisions); + const action = "maintenance.synthetic-cleanup.object-delete-retry"; + if ( + !approval || + approval.status !== "approved" || + approval.actionType !== action || + approval.requiredCapability !== "administration.manage" || + approval.requiredApprovalCount !== + actionApprovalPolicy[action].approvalCount || + approval.requestingActorId !== manifest.maintenanceActorId || + approval.expiresAt <= new Date() || + approval.executedAt !== null || + !target.success || + target.data.manifestId !== manifest.manifestId || + target.data.manifestDigest !== manifest.digest || + target.data.pendingObjectDigest !== pendingDigest || + !decisions.success + ) { + throw new Error("Executable cleanup object retry approval is missing"); + } + assertExecutableApproval(action, decisions.data); + const approvedActorIds = [ + ...new Set( + decisions.data + .filter((decision) => decision.status === "approved") + .map((decision) => decision.actorId), + ), + ]; + if (approvedActorIds.includes(manifest.maintenanceActorId)) { + throw new Error( + "Cleanup object retry requires an independent approver", + ); + } + const approvers = await tx + .select({ + id: schema.actors.id, + status: schema.actors.status, + capabilities: schema.actors.capabilityAssignments, + }) + .from(schema.actors) + .where( + and( + eq(schema.actors.organisationId, manifest.organisationId), + inArray(schema.actors.id, approvedActorIds), + ), + ) + .for("update"); + if ( + approvers.length !== approvedActorIds.length || + approvers.some((approver) => { + const capabilities = z + .array(z.string()) + .safeParse(approver.capabilities); + return ( + approver.status !== "active" || + !capabilities.success || + !capabilities.data.includes("administration.manage") + ); + }) + ) { + throw new Error("Cleanup object retry approver capability was revoked"); + } + const [executed] = await tx + .update(schema.approvals) + .set({ status: "executed", executedAt: new Date() }) + .where( + and( + eq(schema.approvals.organisationId, manifest.organisationId), + eq(schema.approvals.id, retry.retryApprovalId), + eq(schema.approvals.status, "approved"), + ), + ) + .returning({ id: schema.approvals.id }); + if (!executed) { + throw new Error("Cleanup object retry approval execution changed"); + } + await appendAuditEvent(tx, { + organisationId: manifest.organisationId, + actorId: manifest.maintenanceActorId, + actorType: "human", + action: "maintenance.synthetic_cleanup.object_retry_authorised", + targetType: "cleanup_manifest", + targetId: manifest.manifestId, + metadata: { + retryApprovalId: retry.retryApprovalId, + pendingObjectDigest: pendingDigest, + pendingObjectVersions: pending.length, + }, + traceId, + }); + await writeOutbox(tx, { + organisationId: manifest.organisationId, + eventType: "maintenance.synthetic_cleanup.object_retry_authorised", + aggregateType: "cleanup_manifest", + aggregateId: manifest.manifestId, + queueName: "muster-outbox", + payload: { + manifestId: manifest.manifestId, + retryApprovalId: retry.retryApprovalId, + pendingObjectDigest: pendingDigest, + }, + idempotencyKey: `maintenance.synthetic-cleanup:${manifest.manifestId}:object-retry:${retry.retryApprovalId}:authorised`, + traceId, + }); + if (pending.length) { + await writeOutbox(tx, { + organisationId: manifest.organisationId, + eventType: "maintenance.synthetic_cleanup.object_delete.queued", + aggregateType: "cleanup_object_retry_approval", + aggregateId: retry.retryApprovalId, + queueName: "muster-maintenance", + payload: { + manifestId: manifest.manifestId, + authorizationApprovalId: retry.retryApprovalId, + }, + idempotencyKey: `maintenance.synthetic-cleanup:${manifest.manifestId}:object-retry:${retry.retryApprovalId}:queued`, + traceId, + }); + } + return { authorised: true, pendingObjects: pending }; + }, + { isolationLevel: "serializable", accessMode: "read write" }, + ); +} + +export async function recordSyntheticCleanupObjectDeletionAttempt( + subject: AuthorisationSubject, + input: unknown, + object: SyntheticCleanupObject, + authorizationApprovalId: string, + result: "started" | "succeeded" | "failed" | "observed_missing", + traceId: string, + errorCode?: string, + db = database(), +) { + const manifest = parseSyntheticCleanupManifest(input); + validateAuthenticatedSubject(subject, manifest); + const expected = manifest.objectStorageObjects.find( + (candidate) => candidate.evidenceId === object.evidenceId, + ); + if (!expected || canonical(expected) !== canonical(object)) { + throw new Error("Cleanup deletion attempt object is not manifest-bound"); } - const input = JSON.parse(await readFile(manifestPath, "utf8")) as unknown; - const result = await applySyntheticCleanup(input, `maintenance-${newId()}`); - process.stdout.write(`${JSON.stringify(result)}\n`); + return db.transaction(async (tx) => { + const [receipt] = await tx + .select({ + digest: schema.syntheticCleanupReceipts.manifestDigest, + approvalId: schema.syntheticCleanupReceipts.approvalId, + }) + .from(schema.syntheticCleanupReceipts) + .where( + and( + eq( + schema.syntheticCleanupReceipts.organisationId, + manifest.organisationId, + ), + eq(schema.syntheticCleanupReceipts.manifestId, manifest.manifestId), + ), + ) + .limit(1); + if (!receipt || receipt.digest !== manifest.digest) { + throw new Error("Cleanup receipt is unavailable for object deletion"); + } + const [approval] = await tx + .select({ + id: schema.approvals.id, + actionType: schema.approvals.actionType, + requestingActorId: schema.approvals.requestingActorId, + status: schema.approvals.status, + target: schema.approvals.target, + }) + .from(schema.approvals) + .where( + and( + eq(schema.approvals.organisationId, manifest.organisationId), + eq(schema.approvals.id, authorizationApprovalId), + ), + ) + .limit(1); + const originalTarget = z + .object({ manifestId: z.uuid(), digest }) + .safeParse(approval?.target); + const retryTarget = z + .object({ manifestId: z.uuid(), manifestDigest: digest }) + .safeParse(approval?.target); + const originalApproval = + approval?.id === receipt.approvalId && + approval.actionType === "maintenance.synthetic-cleanup" && + originalTarget.success && + originalTarget.data.manifestId === manifest.manifestId && + originalTarget.data.digest === manifest.digest; + const retryApproval = + approval?.actionType === + "maintenance.synthetic-cleanup.object-delete-retry" && + retryTarget.success && + retryTarget.data.manifestId === manifest.manifestId && + retryTarget.data.manifestDigest === manifest.digest; + if ( + !approval || + approval.status !== "executed" || + approval.requestingActorId !== manifest.maintenanceActorId || + (!originalApproval && !retryApproval) + ) { + throw new Error("Cleanup object deletion approval is not executable"); + } + const [attempt] = await tx + .insert(schema.syntheticCleanupObjectDeletionAttempts) + .values({ + id: newId(), + manifestId: manifest.manifestId, + organisationId: manifest.organisationId, + evidenceId: object.evidenceId, + versionId: object.versionId, + authorizationApprovalId, + result, + errorCode: errorCode?.slice(0, 200), + attemptedByActorId: subject.actorId, + traceId, + }) + .returning(); + if (!attempt) throw new Error("Cleanup deletion attempt was not recorded"); + return attempt; + }); } -if (process.argv[1]?.endsWith("synthetic-cleanup.ts")) { - await main().finally(closeDatabase); +export async function applySyntheticCleanup( + subject: AuthorisationSubject, + input: unknown, + traceId: string, + db = database(), +) { + const manifest = parseSyntheticCleanupManifest(input); + validateAuthenticatedSubject(subject, manifest); + try { + return await db.transaction( + async (tx) => { + await tx.execute( + sql`select pg_advisory_xact_lock(hashtextextended(${manifest.manifestId}, 0))`, + ); + const [prior] = await tx + .select() + .from(schema.syntheticCleanupReceipts) + .where( + and( + eq( + schema.syntheticCleanupReceipts.organisationId, + manifest.organisationId, + ), + eq( + schema.syntheticCleanupReceipts.manifestId, + manifest.manifestId, + ), + ), + ) + .limit(1); + if (prior) { + if (prior.manifestDigest !== manifest.digest) { + throw new Error("Completed cleanup manifest digest changed"); + } + return { + applied: false, + manifestId: manifest.manifestId, + receipt: prior, + }; + } + + const now = new Date(); + await validateMaintenanceActor(tx, manifest); + await validateExecutableApproval(tx, manifest, now); + const rows = await loadCandidateRows(tx, manifest); + verifyCandidateRows(manifest, rows); + const counts = candidateCounts(rows); + const idempotencyKey = `maintenance.synthetic-cleanup:${manifest.manifestId}`; + + await applyCandidateTransitions(tx, manifest, now, idempotencyKey); + const postRows = await loadCandidateRows(tx, manifest); + const postDigests = verifyPostCandidateStates( + manifest, + rows, + postRows, + now, + ); + await verifyMessageRevisions(tx, manifest, rows, idempotencyKey); + const executed = await tx + .update(schema.approvals) + .set({ status: "executed", executedAt: now }) + .where( + and( + eq(schema.approvals.organisationId, manifest.organisationId), + eq(schema.approvals.id, manifest.approvalId), + eq(schema.approvals.status, "approved"), + ), + ) + .returning({ id: schema.approvals.id }); + if (executed.length !== 1) { + throw new Error("Cleanup approval execution changed"); + } + await tx.insert(schema.syntheticCleanupReceipts).values({ + manifestId: manifest.manifestId, + organisationId: manifest.organisationId, + approvalId: manifest.approvalId, + maintenanceActorId: manifest.maintenanceActorId, + manifestDigest: manifest.digest, + manifest, + candidateCounts: counts, + preDigests: manifest.tableDigests, + postDigests, + objectStorageObjects: manifest.objectStorageObjects, + traceId, + appliedAt: now, + }); + await appendAuditEvent(tx, { + organisationId: manifest.organisationId, + actorId: manifest.maintenanceActorId, + actorType: "human", + action: "maintenance.synthetic_cleanup.applied", + targetType: "cleanup_manifest", + targetId: manifest.manifestId, + metadata: { + approvalId: manifest.approvalId, + digest: manifest.digest, + candidateCounts: counts, + preDigests: manifest.tableDigests, + postDigests, + objectStorageObjectCount: manifest.objectStorageObjects.length, + }, + traceId, + }); + await writeOutbox(tx, { + organisationId: manifest.organisationId, + eventType: "maintenance.synthetic_cleanup.applied", + aggregateType: "cleanup_manifest", + aggregateId: manifest.manifestId, + queueName: "muster-outbox", + payload: { + manifestId: manifest.manifestId, + digest: manifest.digest, + candidateCounts: counts, + objectStorageObjects: manifest.objectStorageObjects, + }, + idempotencyKey, + traceId, + }); + if (manifest.objectStorageObjects.length) { + await writeOutbox(tx, { + organisationId: manifest.organisationId, + eventType: "maintenance.synthetic_cleanup.object_delete.queued", + aggregateType: "cleanup_manifest", + aggregateId: manifest.manifestId, + queueName: "muster-maintenance", + payload: { + manifestId: manifest.manifestId, + authorizationApprovalId: manifest.approvalId, + }, + idempotencyKey: `${idempotencyKey}:object-delete:queued`, + traceId, + }); + } + return { + applied: true, + manifestId: manifest.manifestId, + candidateCounts: counts, + preDigests: manifest.tableDigests, + postDigests, + objectStorageObjects: manifest.objectStorageObjects, + }; + }, + { isolationLevel: "serializable", accessMode: "read write" }, + ); + } catch (error) { + const cause = + error && typeof error === "object" && "cause" in error + ? (error as { cause?: unknown }).cause + : undefined; + const code = + cause && typeof cause === "object" && "code" in cause + ? (cause as { code?: unknown }).code + : undefined; + if (code === "40001") { + const receipt = await findSyntheticCleanupReceipt(subject, manifest, db); + if (receipt) { + return { + applied: false, + manifestId: manifest.manifestId, + receipt, + }; + } + } + throw error; + } } diff --git a/packages/evidence/src/index.ts b/packages/evidence/src/index.ts index fc5b62d..619547b 100644 --- a/packages/evidence/src/index.ts +++ b/packages/evidence/src/index.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +export * from "./object-storage.ts"; + export const evidenceMimeAllowlist = [ "application/json", "application/pdf", @@ -12,20 +14,37 @@ export const evidenceMimeAllowlist = [ export const EvidenceUploadRequestSchema = z.object({ organisationId: z.string().uuid(), - fileName: z.string().min(1).max(255).refine((value) => !/[\/\\\0]/.test(value), "Unsafe file name"), + fileName: z + .string() + .min(1) + .max(255) + .refine((value) => !/[\/\\\0]/.test(value), "Unsafe file name"), mimeType: z.enum(evidenceMimeAllowlist), - size: z.number().int().positive().max(250 * 1024 * 1024), + size: z + .number() + .int() + .positive() + .max(250 * 1024 * 1024), sha256: z.string().regex(/^[a-f0-9]{64}$/), - classification: z.enum(["public","internal","confidential","restricted"]), + classification: z.enum(["public", "internal", "confidential", "restricted"]), }); export interface ObjectStorageAdapter { - presignUpload(storageKey: string, mimeType: string, size: number, expiresSeconds: number): Promise; + presignUpload( + storageKey: string, + mimeType: string, + size: number, + expiresSeconds: number, + ): Promise; presignDownload(storageKey: string, expiresSeconds: number): Promise; quarantine(storageKey: string): Promise; } -export function evidenceStorageKey(organisationId: string, evidenceId: string, fileName: string) { +export function evidenceStorageKey( + organisationId: string, + evidenceId: string, + fileName: string, +) { const safeName = fileName.normalize("NFKC").replace(/[^a-zA-Z0-9._-]/g, "_"); return `organisations/${organisationId}/evidence/${evidenceId}/${safeName}`; } diff --git a/packages/evidence/src/object-storage.test.ts b/packages/evidence/src/object-storage.test.ts new file mode 100644 index 0000000..53b0ebd --- /dev/null +++ b/packages/evidence/src/object-storage.test.ts @@ -0,0 +1,35 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { defaultObjectStorage } from "./object-storage.ts"; + +describe("versioned object storage", () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); + }); + + it("downloads the exact immutable version", async () => { + vi.stubEnv("OBJECT_STORAGE_ENDPOINT", "http://127.0.0.1:9000"); + vi.stubEnv("OBJECT_STORAGE_BUCKET", "muster-evidence"); + const body = new TextEncoder().encode("exact version"); + const fetchMock = vi.fn().mockResolvedValue( + new Response(body, { + status: 200, + headers: { "content-type": "application/octet-stream" }, + }), + ); + vi.stubGlobal("fetch", fetchMock); + + await expect( + defaultObjectStorage.getObjectVersion( + "organisations/test/evidence.bin", + "immutable-version-1", + ), + ).resolves.toEqual(body); + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + search: "?versionId=immutable-version-1", + }), + expect.objectContaining({ method: "GET" }), + ); + }); +}); diff --git a/packages/evidence/src/object-storage.ts b/packages/evidence/src/object-storage.ts new file mode 100644 index 0000000..5099725 --- /dev/null +++ b/packages/evidence/src/object-storage.ts @@ -0,0 +1,270 @@ +import { createHash, createHmac } from "node:crypto"; + +export type EvidenceObject = { + storageKey: string; + contentType: string; + body: Uint8Array; +}; + +export interface EvidenceObjectStorage { + putObject(object: EvidenceObject): Promise; +} + +export interface ContentObjectStorage extends EvidenceObjectStorage { + getObject(storageKey: string): Promise; +} + +export interface CleanupObjectStorage extends ContentObjectStorage { + getObjectVersion(storageKey: string, versionId: string): Promise; + headObject( + storageKey: string, + versionId: string, + ): Promise<{ + size: number; + etag: string; + versionId: string; + legalHold: boolean; + objectLockMetadata: Record; + } | null>; + deleteObject(storageKey: string, versionId: string): Promise; +} + +function sha256(value: string | Uint8Array) { + return createHash("sha256").update(value).digest("hex"); +} + +function hmac(key: string | Buffer, value: string) { + return createHmac("sha256", key).update(value).digest(); +} + +function objectUrl(endpoint: string, bucket: string, storageKey: string) { + const url = new URL(endpoint); + const basePath = url.pathname.replace(/\/$/, ""); + const encodedKey = storageKey + .split("/") + .map((part) => encodeURIComponent(part)) + .join("/"); + url.pathname = `${basePath}/${encodeURIComponent(bucket)}/${encodedKey}`; + return url; +} + +function signingHeaders( + method: "DELETE" | "GET" | "HEAD" | "PUT", + url: URL, + body: Uint8Array, + region: string, + accessKey: string, + secretKey: string, + contentType?: string, +) { + const now = new Date(); + const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, ""); + const date = amzDate.slice(0, 8); + const payloadHash = sha256(body); + const canonicalHeaders = [ + ...(contentType ? [`content-type:${contentType}`] : []), + `host:${url.host}`, + `x-amz-content-sha256:${payloadHash}`, + `x-amz-date:${amzDate}`, + ].join("\n"); + const signedHeaders = contentType + ? "content-type;host;x-amz-content-sha256;x-amz-date" + : "host;x-amz-content-sha256;x-amz-date"; + const canonicalRequest = [ + method, + url.pathname, + [...url.searchParams.entries()] + .sort(([left], [right]) => left.localeCompare(right)) + .map( + ([key, value]) => + `${encodeURIComponent(key)}=${encodeURIComponent(value)}`, + ) + .join("&"), + canonicalHeaders, + "", + signedHeaders, + payloadHash, + ].join("\n"); + const scope = `${date}/${region}/s3/aws4_request`; + const stringToSign = [ + "AWS4-HMAC-SHA256", + amzDate, + scope, + sha256(canonicalRequest), + ].join("\n"); + const dateKey = hmac(`AWS4${secretKey}`, date); + const regionKey = hmac(dateKey, region); + const serviceKey = hmac(regionKey, "s3"); + const signingKey = hmac(serviceKey, "aws4_request"); + const signature = createHmac("sha256", signingKey) + .update(stringToSign) + .digest("hex"); + return { + ...(contentType ? { "content-type": contentType } : {}), + "x-amz-content-sha256": payloadHash, + "x-amz-date": amzDate, + authorization: + `AWS4-HMAC-SHA256 Credential=${accessKey}/${scope},` + + ` SignedHeaders=${signedHeaders}, Signature=${signature}`, + }; +} + +function storageConfiguration() { + return { + endpoint: process.env.OBJECT_STORAGE_ENDPOINT ?? "http://127.0.0.1:9000", + bucket: process.env.OBJECT_STORAGE_BUCKET ?? "muster-evidence", + region: process.env.OBJECT_STORAGE_REGION ?? "us-east-1", + accessKey: process.env.OBJECT_STORAGE_ACCESS_KEY ?? "muster", + secretKey: process.env.OBJECT_STORAGE_SECRET_KEY ?? "local-minio-secret", + }; +} + +export const defaultObjectStorage: CleanupObjectStorage = { + async putObject(object) { + const { endpoint, bucket, region, accessKey, secretKey } = + storageConfiguration(); + const url = objectUrl(endpoint, bucket, object.storageKey); + const response = await fetch(url, { + method: "PUT", + headers: signingHeaders( + "PUT", + url, + object.body, + region, + accessKey, + secretKey, + object.contentType, + ), + body: Buffer.from(object.body), + }); + if (!response.ok) { + throw new Error( + `Object storage rejected upload with status ${response.status}`, + ); + } + }, + async getObject(storageKey) { + const { endpoint, bucket, region, accessKey, secretKey } = + storageConfiguration(); + const url = objectUrl(endpoint, bucket, storageKey); + const emptyBody = new Uint8Array(); + const response = await fetch(url, { + method: "GET", + headers: signingHeaders( + "GET", + url, + emptyBody, + region, + accessKey, + secretKey, + ), + }); + if (!response.ok) { + throw new Error( + `Object storage rejected download with status ${response.status}`, + ); + } + return new Uint8Array(await response.arrayBuffer()); + }, + async headObject(storageKey, versionId) { + const { endpoint, bucket, region, accessKey, secretKey } = + storageConfiguration(); + const url = objectUrl(endpoint, bucket, storageKey); + if (versionId !== "unversioned") { + url.searchParams.set("versionId", versionId); + } + const emptyBody = new Uint8Array(); + const response = await fetch(url, { + method: "HEAD", + headers: signingHeaders( + "HEAD", + url, + emptyBody, + region, + accessKey, + secretKey, + ), + }); + if (response.status === 404) return null; + if (!response.ok) { + throw new Error( + `Object storage rejected metadata request with status ${response.status}`, + ); + } + const size = Number(response.headers.get("content-length")); + const etag = response.headers.get("etag")?.replace(/^"|"$/g, ""); + const actualVersionId = + response.headers.get("x-amz-version-id") ?? "unversioned"; + if (!Number.isSafeInteger(size) || size < 0 || !etag || !actualVersionId) { + throw new Error("Object storage returned incomplete version metadata"); + } + const objectLockMetadata = Object.fromEntries( + ["x-amz-object-lock-mode", "x-amz-object-lock-retain-until-date"] + .map((header) => [header, response.headers.get(header)] as const) + .filter((entry): entry is readonly [string, string] => + Boolean(entry[1]), + ), + ); + return { + size, + etag, + versionId: actualVersionId, + legalHold: + response.headers.get("x-amz-object-lock-legal-hold")?.toUpperCase() === + "ON", + objectLockMetadata, + }; + }, + async getObjectVersion(storageKey, versionId) { + const { endpoint, bucket, region, accessKey, secretKey } = + storageConfiguration(); + const url = objectUrl(endpoint, bucket, storageKey); + url.searchParams.set("versionId", versionId); + const emptyBody = new Uint8Array(); + const response = await fetch(url, { + method: "GET", + headers: signingHeaders( + "GET", + url, + emptyBody, + region, + accessKey, + secretKey, + ), + }); + if (!response.ok) { + throw new Error( + `Object storage rejected version download with status ${response.status}`, + ); + } + return new Uint8Array(await response.arrayBuffer()); + }, + async deleteObject(storageKey, versionId) { + const { endpoint, bucket, region, accessKey, secretKey } = + storageConfiguration(); + const url = objectUrl(endpoint, bucket, storageKey); + if (versionId !== "unversioned") { + url.searchParams.set("versionId", versionId); + } + const emptyBody = new Uint8Array(); + const response = await fetch(url, { + method: "DELETE", + headers: signingHeaders( + "DELETE", + url, + emptyBody, + region, + accessKey, + secretKey, + ), + }); + if (!response.ok && response.status !== 404) { + throw new Error( + `Object storage rejected version deletion with status ${response.status}`, + ); + } + }, +}; + +export const defaultEvidenceObjectStorage: EvidenceObjectStorage = + defaultObjectStorage; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d95cb5..d94adf9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -216,6 +216,9 @@ importers: '@muster/database': specifier: workspace:* version: link:../../packages/database + '@muster/evidence': + specifier: workspace:* + version: link:../../packages/evidence '@muster/integrations': specifier: workspace:* version: link:../../packages/integrations @@ -394,6 +397,9 @@ importers: '@muster/audit': specifier: workspace:* version: link:../audit + '@muster/authz': + specifier: workspace:* + version: link:../authz '@muster/contracts': specifier: workspace:* version: link:../contracts From a86759c97cbe1d987e3cad6ccb987056a43cf4f5 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 11:01:34 +1000 Subject: [PATCH 12/47] ci: harden immutable homelab release handoff --- .github/workflows/ci.yml | 20 ++++++++++++++++++ deploy/docker/.env.homelab.example | 3 ++- deploy/docker/README.md | 3 +++ docs/operations/release-homelab.md | 33 ++++++++++++++++++++++++++++++ scripts/install-homelab.sh | 6 ++++++ 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 docs/operations/release-homelab.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be85dc5..f1387b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,6 +149,24 @@ jobs: sbom: ${{ github.event_name == 'push' }} cache-from: type=gha cache-to: type=gha,mode=max + - name: Scan built image with Trivy + run: | + docker run --rm \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy:0.67.2 image \ + --exit-code 1 --ignore-unfixed --severity HIGH,CRITICAL \ + "${REGISTRY}/${IMAGE_NAME}:sha-${GITHUB_SHA::7}" + - name: Record immutable image digest + if: github.event_name == 'push' + run: | + printf '%s %s\n' "${{ steps.build.outputs.digest }}" \ + "${REGISTRY}/${IMAGE_NAME}:sha-${GITHUB_SHA::7}" > muster-image.sha256 + - name: Upload immutable image checksum + if: github.event_name == 'push' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: muster-image-sha256 + path: muster-image.sha256 - name: Verify container starts if: github.event_name == 'pull_request' shell: bash @@ -169,6 +187,8 @@ jobs: public_image="${REGISTRY}/${IMAGE_NAME,,}:sha-${GITHUB_SHA::7}" docker logout "$REGISTRY" docker pull "$public_image" + docker image inspect "$public_image" \ + --format '{{.Os}}/{{.Architecture}}' | grep -x 'linux/amd64' - name: Publication summary if: github.event_name == 'push' run: | diff --git a/deploy/docker/.env.homelab.example b/deploy/docker/.env.homelab.example index 3a84c41..794636e 100644 --- a/deploy/docker/.env.homelab.example +++ b/deploy/docker/.env.homelab.example @@ -4,7 +4,8 @@ AUTH_TRUSTED_ORIGINS=http://muster.example.lan:3004,http://homelab:3004 MUSTER_ORGANISATION_NAME=Muster Workspace MUSTER_ORGANISATION_SLUG=muster MUSTER_DEFAULT_TIMEZONE=Australia/Sydney -MUSTER_VERSION=latest +# Required: immutable public GHCR tag, for example sha-abc1234. +MUSTER_VERSION=sha-REPLACE-WITH-REVIEWED-COMMIT AUTH_SECURE_COOKIES=false POSTGRES_PASSWORD=generate-postgres-password BETTER_AUTH_SECRET=generate-better-auth-secret diff --git a/deploy/docker/README.md b/deploy/docker/README.md index 8685751..5356eaf 100644 --- a/deploy/docker/README.md +++ b/deploy/docker/README.md @@ -21,3 +21,6 @@ Only Muster's configured HTTP port is published. The example uses `http://muster.example.lan:3004`; replace it with the exact trusted browser origin. Use an HTTPS reverse proxy and set `AUTH_SECURE_COOKIES=true` when exposing Muster beyond a trusted local network. + +Use the immutable-tag release, smoke, and rollback procedure in +[release-homelab.md](../../docs/operations/release-homelab.md). diff --git a/docs/operations/release-homelab.md b/docs/operations/release-homelab.md new file mode 100644 index 0000000..d3c8f43 --- /dev/null +++ b/docs/operations/release-homelab.md @@ -0,0 +1,33 @@ +# Immutable release and homelab handoff + +Release only after the CI quality and container jobs are green. Record the +published `sha-` tag and OCI digest from the workflow checksum artifact. +The container pipeline builds a single `linux/amd64` application image, emits +SBOM/provenance, scans it with Trivy, and verifies an anonymous GHCR pull after +logout. + +On the private homelab, preserve the previous immutable tag before changing +anything: + +```bash +grep '^MUSTER_VERSION=' .env.homelab +MUSTER_VERSION=sha-REPLACE-WITH-REVIEWED-COMMIT ./scripts/install-homelab.sh +docker compose --env-file .env.homelab -f deploy/docker/docker-compose.homelab.yml ps +curl --fail http://127.0.0.1:3004/api/v1/health +``` + +The installer requires an immutable `sha-` value. It creates a private +administrator only for this homelab and does not seed demo activity. Keep +`MUSTER_PUBLIC_URL` and `AUTH_TRUSTED_ORIGINS` limited to the exact homelab/IP +browser origins. The Codex state volume is retained by Compose; authenticate it +only through the `codex-login` setup profile. + +Rollback only after checking migration compatibility and preserving state: + +```bash +MUSTER_VERSION=sha-PREVIOUS-REVIEWED-COMMIT ./scripts/install-homelab.sh +``` + +Do not rollback across an incompatible database migration. Follow +[backup and restore](backup-restore.md) for a stateful recovery, and run the +private homelab login/message/task/Codex smoke suite after any release. diff --git a/scripts/install-homelab.sh b/scripts/install-homelab.sh index b7f422e..239a214 100755 --- a/scripts/install-homelab.sh +++ b/scripts/install-homelab.sh @@ -43,6 +43,7 @@ if [[ ! -f "$env_file" ]]; then sed -i.bak \ -e "s|MUSTER_PUBLIC_URL=.*|MUSTER_PUBLIC_URL=${requested_public_url}|" \ -e "s|MUSTER_HTTP_PORT=.*|MUSTER_HTTP_PORT=${requested_http_port}|" \ + -e "s|MUSTER_VERSION=.*|MUSTER_VERSION=${requested_version}|" \ -e "s|AUTH_TRUSTED_ORIGINS=.*|AUTH_TRUSTED_ORIGINS=${AUTH_TRUSTED_ORIGINS:-${requested_public_url},http://homelab:${requested_http_port}}|" \ -e "s|generate-postgres-password|${postgres_password}|" \ -e "s|generate-better-auth-secret|${auth_secret}|" \ @@ -130,6 +131,11 @@ for external_network in tawny_default kelpie_default; do fi done +if [[ "$MUSTER_VERSION" == "latest" || "$MUSTER_VERSION" == *"REPLACE"* ]]; then + printf '%s\n' 'MUSTER_VERSION must be a reviewed immutable sha- tag.' >&2 + exit 2 +fi + docker compose --env-file "$env_file" -f "$compose_file" pull docker compose --env-file "$env_file" -f "$compose_file" up -d From 9e5074d76ee98bdfef829c4f2c9d0b33e79e561f Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 11:26:40 +1000 Subject: [PATCH 13/47] fix(release): validate immutable image tags --- scripts/install-homelab.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/install-homelab.sh b/scripts/install-homelab.sh index 239a214..a964eef 100755 --- a/scripts/install-homelab.sh +++ b/scripts/install-homelab.sh @@ -131,11 +131,6 @@ for external_network in tawny_default kelpie_default; do fi done -if [[ "$MUSTER_VERSION" == "latest" || "$MUSTER_VERSION" == *"REPLACE"* ]]; then - printf '%s\n' 'MUSTER_VERSION must be a reviewed immutable sha- tag.' >&2 - exit 2 -fi - docker compose --env-file "$env_file" -f "$compose_file" pull docker compose --env-file "$env_file" -f "$compose_file" up -d From c0f1d6e4ebcbfdc9c94f58f912355214fbf02270 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 12:41:53 +1000 Subject: [PATCH 14/47] test(release): verify immutable homelab installer --- deploy/docker/.env.homelab.example | 4 +- deploy/docker/docker-compose.homelab.yml | 2 +- docs/operations/release-homelab.md | 56 ++++++++++--- package.json | 1 + scripts/install-homelab.sh | 101 ++++++++++++++++++----- tests/homelab/install-homelab.test.sh | 82 ++++++++++++++++++ 6 files changed, 210 insertions(+), 36 deletions(-) mode change 100755 => 100644 scripts/install-homelab.sh create mode 100755 tests/homelab/install-homelab.test.sh diff --git a/deploy/docker/.env.homelab.example b/deploy/docker/.env.homelab.example index 794636e..3b64c00 100644 --- a/deploy/docker/.env.homelab.example +++ b/deploy/docker/.env.homelab.example @@ -4,8 +4,8 @@ AUTH_TRUSTED_ORIGINS=http://muster.example.lan:3004,http://homelab:3004 MUSTER_ORGANISATION_NAME=Muster Workspace MUSTER_ORGANISATION_SLUG=muster MUSTER_DEFAULT_TIMEZONE=Australia/Sydney -# Required: immutable public GHCR tag, for example sha-abc1234. -MUSTER_VERSION=sha-REPLACE-WITH-REVIEWED-COMMIT +# Required: immutable public GHCR full commit tag, for example sha-<40-hex-commit>. +MUSTER_VERSION=sha-REPLACE-WITH-REVIEWED-FULL-COMMIT AUTH_SECURE_COOKIES=false POSTGRES_PASSWORD=generate-postgres-password BETTER_AUTH_SECRET=generate-better-auth-secret diff --git a/deploy/docker/docker-compose.homelab.yml b/deploy/docker/docker-compose.homelab.yml index 40ce46a..720825a 100644 --- a/deploy/docker/docker-compose.homelab.yml +++ b/deploy/docker/docker-compose.homelab.yml @@ -34,7 +34,7 @@ x-muster-environment: &muster-environment CODEX_HOME: /var/lib/muster/codex x-muster-service: &muster-service - image: ${MUSTER_IMAGE:-ghcr.io/jusso-dev/muster:${MUSTER_VERSION:-latest}} + image: ${MUSTER_IMAGE:-ghcr.io/jusso-dev/muster:${MUSTER_VERSION:?Set MUSTER_IMAGE to a reviewed digest or MUSTER_VERSION to an immutable sha tag.}} pull_policy: always restart: unless-stopped environment: *muster-environment diff --git a/docs/operations/release-homelab.md b/docs/operations/release-homelab.md index d3c8f43..0abacff 100644 --- a/docs/operations/release-homelab.md +++ b/docs/operations/release-homelab.md @@ -1,31 +1,63 @@ # Immutable release and homelab handoff -Release only after the CI quality and container jobs are green. Record the -published `sha-` tag and OCI digest from the workflow checksum artifact. -The container pipeline builds a single `linux/amd64` application image, emits -SBOM/provenance, scans it with Trivy, and verifies an anonymous GHCR pull after -logout. +Release only after CI quality, security, and container jobs are green. Record +the published full `sha-<40-hex-commit>` tag, OCI digest, SBOM, provenance, and +`SHA256SUMS` from the release artifact. The container pipeline builds one +`linux/amd64` application image, scans it with Trivy, and verifies an anonymous +GHCR pull after logout. + +Before the first release, make the GitHub Container Registry package public in +its package settings. CI deliberately logs out before pulling; it fails until +anonymous users can pull the package. Do not treat a successful authenticated +pull as this gate. On the private homelab, preserve the previous immutable tag before changing anything: ```bash grep '^MUSTER_VERSION=' .env.homelab -MUSTER_VERSION=sha-REPLACE-WITH-REVIEWED-COMMIT ./scripts/install-homelab.sh +MUSTER_VERSION=sha-REPLACE-WITH-REVIEWED-FULL-COMMIT ./scripts/install-homelab.sh docker compose --env-file .env.homelab -f deploy/docker/docker-compose.homelab.yml ps curl --fail http://127.0.0.1:3004/api/v1/health +curl --fail http://127.0.0.1:3004/api/v1/ready ``` -The installer requires an immutable `sha-` value. It creates a private -administrator only for this homelab and does not seed demo activity. Keep -`MUSTER_PUBLIC_URL` and `AUTH_TRUSTED_ORIGINS` limited to the exact homelab/IP -browser origins. The Codex state volume is retained by Compose; authenticate it -only through the `codex-login` setup profile. +The installer requires a full immutable `sha-<40-hex-commit>` tag. It writes +the provided tag into `.env.homelab`, so upgrade and rollback commands cannot +silently retain an old tag. It creates a private administrator only for this +homelab, prints its password only when generated, and does not seed demo +activity. Keep `MUSTER_PUBLIC_URL` and `AUTH_TRUSTED_ORIGINS` limited to exact +IP and `homelab` browser origins; pass both variables explicitly if deployment +uses a different approved pair. + +The private `codex-state` volume survives application upgrades and rollback. +Authenticate it only through the setup profile; never copy, publish, or log +`auth.json`: + +```bash +docker compose --env-file .env.homelab -f deploy/docker/docker-compose.homelab.yml \ + --profile setup run --rm codex-login +docker compose --env-file .env.homelab -f deploy/docker/docker-compose.homelab.yml \ + exec -T agent-gateway /nodejs/bin/node -e \ + 'fetch("http://127.0.0.1:3002/ready").then((r) => r.json()).then(({ authenticated, status }) => console.log({ authenticated, status }))' +``` + +After the gateway reports `authenticated: true`, run the private Chromium smoke +suite with injected private credentials and no captured artifacts. It covers +login, message send, task creation, and the configured agent runtime; do not +put those values in source control or shell history: + +```bash +MUSTER_BASE_URL=http://homelab:3004 MUSTER_HOMELAB_CRITICAL=true \ +MUSTER_CAPTURE_ARTIFACTS=false MUSTER_LOCAL_ADMIN_EMAIL=... \ +MUSTER_LOCAL_ADMIN_PASSWORD=... MUSTER_SECONDARY_EMAIL=... \ +MUSTER_SECONDARY_PASSWORD=... pnpm test:e2e:homelab --project=chromium +``` Rollback only after checking migration compatibility and preserving state: ```bash -MUSTER_VERSION=sha-PREVIOUS-REVIEWED-COMMIT ./scripts/install-homelab.sh +MUSTER_VERSION=sha-PREVIOUS-REVIEWED-FULL-COMMIT ./scripts/install-homelab.sh ``` Do not rollback across an incompatible database migration. Follow diff --git a/package.json b/package.json index 32a999b..5cd5ce9 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test:e2e": "playwright test", "test:e2e:homelab": "playwright test --config=playwright.homelab.config.ts", "test:homelab-installer": "bash scripts/test-install-homelab.sh", + "test:release-homelab": "bash tests/homelab/install-homelab.test.sh", "contracts:generate": "pnpm --filter @muster/contracts generate", "db:generate": "pnpm --filter @muster/database generate", "db:migrate": "pnpm --filter @muster/database migrate", diff --git a/scripts/install-homelab.sh b/scripts/install-homelab.sh old mode 100755 new mode 100644 index a964eef..f82cbb2 --- a/scripts/install-homelab.sh +++ b/scripts/install-homelab.sh @@ -31,6 +31,30 @@ if [[ -n "$requested_image" && ! "$requested_image" =~ ^ghcr\.io/jusso-dev/muste exit 2 fi +env_value() { + local key="$1" + sed -n "s/^${key}=//p" "$env_file" | tail -n 1 +} + +upsert_env() { + local key="$1" + local value="$2" + local temporary_file="${env_file}.tmp" + + if [[ "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then + printf 'Refusing multiline value for %s.\n' "$key" >&2 + exit 2 + fi + + awk -v key="$key" -v value="$value" ' + index($0, key "=") == 1 { print key "=" value; seen = 1; next } + { print } + END { if (!seen) print key "=" value } + ' "$env_file" > "$temporary_file" + mv "$temporary_file" "$env_file" +} + +credentials_created=false if [[ ! -f "$env_file" ]]; then cp deploy/docker/.env.homelab.example "$env_file" postgres_password="$(openssl rand -hex 24)" @@ -58,11 +82,16 @@ if [[ ! -f "$env_file" ]]; then printf 'MUSTER_LOCAL_ADMIN_PASSWORD=%s\n' "$admin_password" } >> "$env_file" chmod 600 "$env_file" + credentials_created=true fi -if ! grep -q '^AUTH_TRUSTED_ORIGINS=' "$env_file"; then - printf 'AUTH_TRUSTED_ORIGINS=%s,http://homelab:%s\n' \ - "$requested_public_url" "$requested_http_port" >> "$env_file" +if [[ -z "$(env_value MUSTER_LOCAL_ADMIN_EMAIL)" ]]; then + upsert_env MUSTER_LOCAL_ADMIN_EMAIL "${MUSTER_LOCAL_ADMIN_EMAIL:-admin@muster.local}" + credentials_created=true +fi +if [[ -z "$(env_value MUSTER_LOCAL_ADMIN_PASSWORD)" ]]; then + upsert_env MUSTER_LOCAL_ADMIN_PASSWORD "Muster!$(openssl rand -hex 12)" + credentials_created=true fi if ! grep -q '^MUSTER_AGENT_GATEWAY_TOKEN=' "$env_file"; then @@ -131,28 +160,52 @@ for external_network in tawny_default kelpie_default; do fi done +http_port="$MUSTER_HTTP_PORT" +admin_email="$MUSTER_LOCAL_ADMIN_EMAIL" +admin_password="$MUSTER_LOCAL_ADMIN_PASSWORD" + docker compose --env-file "$env_file" -f "$compose_file" pull docker compose --env-file "$env_file" -f "$compose_file" up -d -health_url="http://127.0.0.1:${MUSTER_HTTP_PORT:-3004}/api/v1/health" -for attempt in $(seq 1 60); do - if curl --fail --silent "$health_url" >/dev/null; then - break - fi - if [[ "$attempt" == "60" ]]; then - docker compose --env-file "$env_file" -f "$compose_file" ps - exit 1 - fi - sleep 3 -done +wait_for_endpoint() { + local endpoint="$1" + local label="$2" + local attempt + + for attempt in $(seq 1 60); do + if curl --fail --silent "$endpoint" >/dev/null; then + return 0 + fi + if [[ "$attempt" == "60" ]]; then + printf '%s failed after waiting for 180 seconds.\n' "$label" >&2 + docker compose --env-file "$env_file" -f "$compose_file" ps + return 1 + fi + sleep 3 + done +} + +base_url="http://127.0.0.1:${http_port}" +wait_for_endpoint "${base_url}/api/v1/health" "Health check" +wait_for_endpoint "${base_url}/api/v1/ready" "Readiness check" + +gateway_authentication="$( + docker compose --env-file "$env_file" -f "$compose_file" exec -T agent-gateway \ + /nodejs/bin/node -e ' + fetch("http://127.0.0.1:3002/ready") + .then((response) => response.json()) + .then((body) => process.stdout.write(body.authenticated === true ? "authenticated" : "authentication_required")) + .catch(() => process.exit(1)); + ' 2>/dev/null || true +)" signup_status="$( curl --silent \ --output /dev/null \ --write-out '%{http_code}' \ -H "content-type: application/json" \ - -d "{\"name\":\"Muster Administrator\",\"email\":\"${MUSTER_LOCAL_ADMIN_EMAIL}\",\"password\":\"${MUSTER_LOCAL_ADMIN_PASSWORD}\"}" \ - "http://127.0.0.1:${MUSTER_HTTP_PORT:-3004}/api/auth/sign-up/email" + -d "{\"name\":\"Muster Administrator\",\"email\":\"${admin_email}\",\"password\":\"${admin_password}\"}" \ + "${base_url}/api/auth/sign-up/email" )" if [[ "$signup_status" != "200" && "$signup_status" != "201" && "$signup_status" != "422" ]]; then printf 'Administrator creation failed with HTTP %s.\n' "$signup_status" >&2 @@ -160,9 +213,15 @@ if [[ "$signup_status" != "200" && "$signup_status" != "201" && "$signup_status" fi printf '%s\n' \ - "Muster is ready." \ - "Web: ${MUSTER_PUBLIC_URL}" \ - "Administrator: ${MUSTER_LOCAL_ADMIN_EMAIL}" \ - "Password: ${MUSTER_LOCAL_ADMIN_PASSWORD}" \ - "Codex: copy an authorised auth.json into the private codex-state volume or run the setup profile." \ + "Muster is ready: ${MUSTER_PUBLIC_URL:-$requested_public_url}" \ + "Administrator: ${admin_email}" \ "External products are local mocks and are labelled as such." +if [[ "$gateway_authentication" == "authenticated" ]]; then + printf '%s\n' 'Codex authentication: verified.' +else + printf '%s\n' \ + "Codex authentication: pending. Run docker compose --env-file .env.homelab -f ${compose_file} --profile setup run --rm codex-login." +fi +if [[ "$credentials_created" == "true" ]]; then + printf 'New private homelab administrator password: %s\n' "$admin_password" +fi diff --git a/tests/homelab/install-homelab.test.sh b/tests/homelab/install-homelab.test.sh new file mode 100755 index 0000000..f689891 --- /dev/null +++ b/tests/homelab/install-homelab.test.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "$0")/../.." && pwd)" +temporary_directory="$(mktemp -d)" +trap 'rm -rf "$temporary_directory"' EXIT + +fixture="$temporary_directory/muster" +mkdir -p "$fixture" "$temporary_directory/bin" +cp -R "$root/deploy" "$fixture/deploy" +cp -R "$root/scripts" "$fixture/scripts" + +export TEST_LOG="$temporary_directory/commands.log" +export PATH="$temporary_directory/bin:$PATH" + +cat > "$temporary_directory/bin/docker" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$*" >> "$TEST_LOG" +EOF +cat > "$temporary_directory/bin/curl" <<'EOF' +#!/usr/bin/env bash +printf '%s\n' "$*" >> "$TEST_LOG" +for argument in "$@"; do + if [[ "$argument" == "--write-out" ]]; then + printf '201' + exit 0 + fi +done +EOF +cat > "$temporary_directory/bin/openssl" <<'EOF' +#!/usr/bin/env bash +printf 'synthetic-%s' "$2" +EOF +chmod +x "$temporary_directory/bin/docker" \ + "$temporary_directory/bin/curl" \ + "$temporary_directory/bin/openssl" + +immutable_tag="sha-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +next_immutable_tag="sha-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + +if ( + cd "$fixture" + bash ./scripts/install-homelab.sh >/dev/null 2>&1 +); then + printf 'installer accepted a missing immutable tag\n' >&2 + exit 1 +fi + +first_output="$( + cd "$fixture" + MUSTER_VERSION="$immutable_tag" \ + MUSTER_PUBLIC_URL="http://192.168.1.19:3004" \ + MUSTER_HTTP_PORT=3004 \ + bash ./scripts/install-homelab.sh +)" + +grep -qx "MUSTER_VERSION=${immutable_tag}" "$fixture/.env.homelab" +grep -qx 'AUTH_TRUSTED_ORIGINS=http://192.168.1.19:3004,http://homelab:3004' \ + "$fixture/.env.homelab" +grep -q -- '--env-file .env.homelab -f deploy/docker/docker-compose.homelab.yml pull' \ + "$TEST_LOG" +grep -q '/api/auth/sign-up/email' "$TEST_LOG" +grep -q 'New private homelab administrator password:' <<<"$first_output" +grep -q 'Codex authentication: pending.' <<<"$first_output" + +second_output="$( + cd "$fixture" + MUSTER_VERSION="$next_immutable_tag" \ + MUSTER_PUBLIC_URL="http://192.168.1.20:3004" \ + MUSTER_HTTP_PORT=3004 \ + bash ./scripts/install-homelab.sh +)" + +grep -qx "MUSTER_VERSION=${next_immutable_tag}" "$fixture/.env.homelab" +grep -qx 'AUTH_TRUSTED_ORIGINS=http://192.168.1.20:3004,http://homelab:3004' \ + "$fixture/.env.homelab" +if grep -q 'New private homelab administrator password:' <<<"$second_output"; then + printf 'installer reprinted an existing administrator password\n' >&2 + exit 1 +fi + +printf 'install-homelab test passed\n' From 482958b61d57a591899a7f5ddbbcb5edd11ab332 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 12:41:57 +1000 Subject: [PATCH 15/47] ci(release): attest amd64 image evidence --- .github/workflows/ci.yml | 59 ++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1387b2..3c373c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,7 @@ jobs: mc version enable ci/muster-evidence && mc anonymous set none ci/muster-evidence' - run: pnpm install --frozen-lockfile + - run: pnpm test:release-homelab - name: Build database dependencies run: pnpm exec turbo build --filter=@muster/database - run: pnpm db:migrate @@ -131,7 +132,7 @@ jobs: tags: | type=raw,value=latest,enable={{is_default_branch}} type=ref,event=tag - type=sha + type=sha,format=long labels: | org.opencontainers.image.title=Muster org.opencontainers.image.description=Shared workspace for human and agent-driven security operations @@ -155,18 +156,38 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy:0.67.2 image \ --exit-code 1 --ignore-unfixed --severity HIGH,CRITICAL \ - "${REGISTRY}/${IMAGE_NAME}:sha-${GITHUB_SHA::7}" - - name: Record immutable image digest + "${REGISTRY}/${IMAGE_NAME}:sha-${GITHUB_SHA}" + - name: Generate release SBOM + if: github.event_name == 'push' + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 + with: + image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} + format: cyclonedx-json + output-file: muster-sbom.cdx.json + upload-artifact: false + - name: Generate pull request SBOM + if: github.event_name == 'pull_request' + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 + with: + image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} + format: cyclonedx-json + output-file: muster-sbom.cdx.json + upload-artifact: false + - name: Record immutable image evidence and checksums if: github.event_name == 'push' run: | - printf '%s %s\n' "${{ steps.build.outputs.digest }}" \ - "${REGISTRY}/${IMAGE_NAME}:sha-${GITHUB_SHA::7}" > muster-image.sha256 - - name: Upload immutable image checksum + printf '%s\n' \ + "${REGISTRY}/${IMAGE_NAME}@${{ steps.build.outputs.digest }}" > muster-image.txt + sha256sum muster-sbom.cdx.json muster-image.txt > SHA256SUMS + - name: Upload release evidence if: github.event_name == 'push' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: muster-image-sha256 - path: muster-image.sha256 + name: muster-release-evidence + path: | + muster-sbom.cdx.json + muster-image.txt + SHA256SUMS - name: Verify container starts if: github.event_name == 'pull_request' shell: bash @@ -180,15 +201,31 @@ jobs: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true + - name: Attest published image SBOM + if: github.event_name == 'push' + uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.build.outputs.digest }} + sbom-path: muster-sbom.cdx.json + push-to-registry: true - name: Verify anonymous public pull if: github.event_name == 'push' shell: bash run: | - public_image="${REGISTRY}/${IMAGE_NAME,,}:sha-${GITHUB_SHA::7}" - docker logout "$REGISTRY" + public_image="${REGISTRY}/${IMAGE_NAME,,}:sha-${GITHUB_SHA}" + docker logout "$REGISTRY" || true docker pull "$public_image" docker image inspect "$public_image" \ --format '{{.Os}}/{{.Architecture}}' | grep -x 'linux/amd64' + manifest="$(docker buildx imagetools inspect "$public_image" --raw)" + if jq -e '.manifests' >/dev/null <<<"$manifest"; then + jq -e ' + .manifests | length == 1 and + .[0].platform.os == "linux" and + .[0].platform.architecture == "amd64" + ' >/dev/null <<<"$manifest" + fi - name: Publication summary if: github.event_name == 'push' run: | From 32611e8d62fbc3d9fbc3295ae926e0fa1be86fb3 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 12:42:14 +1000 Subject: [PATCH 16/47] fix(release): protect generated homelab secrets --- scripts/install-homelab.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/install-homelab.sh b/scripts/install-homelab.sh index f82cbb2..fe8770b 100644 --- a/scripts/install-homelab.sh +++ b/scripts/install-homelab.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash set -euo pipefail +umask 077 cd "$(dirname "$0")/.." From 800b21812881f7057aa92208a7e405bc346668b0 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 12:47:32 +1000 Subject: [PATCH 17/47] fix(readiness): report serving dependency health --- apps/web/app/api/v1/ready/route.test.ts | 23 +++++ apps/web/app/api/v1/ready/route.ts | 17 +++- apps/web/lib/object-storage.ts | 1 + apps/web/lib/readiness.test.ts | 59 +++++++++++++ apps/web/lib/readiness.ts | 110 ++++++++++++++++++++++++ docs/openapi.yaml | 6 +- docs/operations/release-homelab.md | 5 ++ packages/evidence/src/object-storage.ts | 34 +++++++- 8 files changed, 249 insertions(+), 6 deletions(-) create mode 100644 apps/web/app/api/v1/ready/route.test.ts create mode 100644 apps/web/lib/readiness.test.ts create mode 100644 apps/web/lib/readiness.ts diff --git a/apps/web/app/api/v1/ready/route.test.ts b/apps/web/app/api/v1/ready/route.test.ts new file mode 100644 index 0000000..7057223 --- /dev/null +++ b/apps/web/app/api/v1/ready/route.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { readinessResponse } from "./route.ts"; + +describe("GET /api/v1/ready", () => { + it("returns non-2xx when a required dependency is unavailable", async () => { + const response = readinessResponse({ + status: "degraded", + dependencies: [ + { name: "postgresql", status: "ready" }, + { name: "redis", status: "unavailable" }, + ], + }); + + expect(response.status).toBe(503); + await expect(response.json()).resolves.toEqual({ + status: "degraded", + dependencies: [ + { name: "postgresql", status: "ready" }, + { name: "redis", status: "unavailable" }, + ], + }); + }); +}); diff --git a/apps/web/app/api/v1/ready/route.ts b/apps/web/app/api/v1/ready/route.ts index 1a3649f..6b9278e 100644 --- a/apps/web/app/api/v1/ready/route.ts +++ b/apps/web/app/api/v1/ready/route.ts @@ -1 +1,16 @@ -export { GET } from "../health/route"; +import { + musterReadiness, + type ReadinessReport, +} from "../../../../lib/readiness.ts"; + +export const dynamic = "force-dynamic"; + +export function readinessResponse(report: ReadinessReport) { + return Response.json(report, { + status: report.status === "ready" ? 200 : 503, + }); +} + +export async function GET() { + return readinessResponse(await musterReadiness()); +} diff --git a/apps/web/lib/object-storage.ts b/apps/web/lib/object-storage.ts index f33f138..720e3f0 100644 --- a/apps/web/lib/object-storage.ts +++ b/apps/web/lib/object-storage.ts @@ -1,4 +1,5 @@ export { + checkObjectStorage, defaultEvidenceObjectStorage, defaultObjectStorage, type CleanupObjectStorage, diff --git a/apps/web/lib/readiness.test.ts b/apps/web/lib/readiness.test.ts new file mode 100644 index 0000000..86571eb --- /dev/null +++ b/apps/web/lib/readiness.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from "vitest"; +import { runReadinessChecks } from "./readiness.ts"; + +describe("readiness checks", () => { + it("reports every healthy serving dependency", async () => { + const report = await runReadinessChecks([ + { name: "postgresql", check: vi.fn().mockResolvedValue(undefined) }, + { name: "redis", check: vi.fn().mockResolvedValue(undefined) }, + { name: "object_storage", check: vi.fn().mockResolvedValue(undefined) }, + { name: "agent_gateway", check: vi.fn().mockResolvedValue(undefined) }, + ]); + + expect(report).toEqual({ + status: "ready", + dependencies: [ + { name: "postgresql", status: "ready" }, + { name: "redis", status: "ready" }, + { name: "object_storage", status: "ready" }, + { name: "agent_gateway", status: "ready" }, + ], + }); + }); + + it("returns a degraded report without exposing dependency errors", async () => { + const report = await runReadinessChecks([ + { name: "postgresql", check: vi.fn().mockResolvedValue(undefined) }, + { + name: "object_storage", + check: vi.fn().mockRejectedValue(new Error("credential=must-not-leak")), + }, + ]); + + expect(report).toEqual({ + status: "degraded", + dependencies: [ + { name: "postgresql", status: "ready" }, + { name: "object_storage", status: "unavailable" }, + ], + }); + expect(JSON.stringify(report)).not.toContain("credential="); + }); + + it("bounds stalled dependencies", async () => { + const report = await runReadinessChecks( + [ + { + name: "redis", + check: () => new Promise(() => undefined), + }, + ], + 10, + ); + + expect(report).toEqual({ + status: "degraded", + dependencies: [{ name: "redis", status: "unavailable" }], + }); + }); +}); diff --git a/apps/web/lib/readiness.ts b/apps/web/lib/readiness.ts new file mode 100644 index 0000000..bed222d --- /dev/null +++ b/apps/web/lib/readiness.ts @@ -0,0 +1,110 @@ +import Redis from "ioredis"; +import { sql } from "drizzle-orm"; +import { database } from "@muster/database"; +import { checkObjectStorage } from "./object-storage.ts"; + +const defaultTimeoutMs = 1_000; + +export type ReadinessDependency = { + name: "postgresql" | "redis" | "object_storage" | "agent_gateway"; + check: (signal: AbortSignal) => Promise; +}; + +export type ReadinessReport = { + status: "ready" | "degraded"; + dependencies: Array<{ + name: ReadinessDependency["name"]; + status: "ready" | "unavailable"; + }>; +}; + +async function withTimeout( + check: (signal: AbortSignal) => Promise, + timeoutMs: number, +) { + const controller = new AbortController(); + let timeout: ReturnType | undefined; + try { + await new Promise((resolve, reject) => { + timeout = setTimeout(() => { + controller.abort(); + reject(new Error("Readiness check timed out")); + }, timeoutMs); + void check(controller.signal).then(resolve, reject); + }); + } finally { + if (timeout) clearTimeout(timeout); + } +} + +export async function runReadinessChecks( + dependencies: readonly ReadinessDependency[], + timeoutMs = defaultTimeoutMs, +): Promise { + const results = await Promise.all( + dependencies.map(async ({ name, check }) => { + try { + await withTimeout(check, timeoutMs); + return { name, status: "ready" as const }; + } catch { + return { name, status: "unavailable" as const }; + } + }), + ); + return { + status: results.every((dependency) => dependency.status === "ready") + ? "ready" + : "degraded", + dependencies: results, + }; +} + +async function checkRedis(signal: AbortSignal) { + const client = new Redis(process.env.REDIS_URL ?? "redis://localhost:6379", { + connectTimeout: defaultTimeoutMs, + enableOfflineQueue: false, + lazyConnect: true, + maxRetriesPerRequest: 1, + retryStrategy: () => null, + }); + client.on("error", () => undefined); + signal.addEventListener("abort", () => client.disconnect(), { once: true }); + try { + await client.connect(); + if ((await client.ping()) !== "PONG") throw new Error("Redis ping failed"); + } finally { + client.disconnect(); + } +} + +function configuredReadinessDependencies(): ReadinessDependency[] { + const dependencies: ReadinessDependency[] = [ + { + name: "postgresql", + check: async () => { + await database().execute(sql`select 1`); + }, + }, + { name: "redis", check: checkRedis }, + { name: "object_storage", check: checkObjectStorage }, + ]; + const agentGatewayUrl = process.env.AGENT_GATEWAY_URL; + if (agentGatewayUrl) { + dependencies.push({ + name: "agent_gateway", + check: async (signal) => { + const response = await fetch(`${agentGatewayUrl}/ready`, { signal }); + if (!response.ok) { + throw new Error( + `Agent gateway readiness returned ${response.status}`, + ); + } + }, + }); + } + return dependencies; +} + +export function musterReadiness() { + return runReadinessChecks(configuredReadinessDependencies()); +} diff --git a/docs/openapi.yaml b/docs/openapi.yaml index e027ecb..e984947 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -14,7 +14,7 @@ paths: operationId: getHealth responses: "200": - description: Process health + description: Web-process liveness and PostgreSQL reachability content: application/json: schema: @@ -25,9 +25,9 @@ paths: operationId: getReadiness responses: "200": - description: Dependencies ready + description: Required serving dependencies ready "503": - description: Dependency unavailable + description: One or more required serving dependencies unavailable /metrics: get: security: [] diff --git a/docs/operations/release-homelab.md b/docs/operations/release-homelab.md index 0abacff..85661f8 100644 --- a/docs/operations/release-homelab.md +++ b/docs/operations/release-homelab.md @@ -22,6 +22,11 @@ curl --fail http://127.0.0.1:3004/api/v1/health curl --fail http://127.0.0.1:3004/api/v1/ready ``` +`/health` is liveness: it only proves the web process can reach PostgreSQL. +`/ready` is a serving-readiness gate: it reports PostgreSQL, Redis/queue, +object storage, and the configured agent gateway without exposing endpoints or +credentials. Any unavailable required dependency returns HTTP 503. + The installer requires a full immutable `sha-<40-hex-commit>` tag. It writes the provided tag into `.env.homelab`, so upgrade and rollback commands cannot silently retain an old tag. It creates a private administrator only for this diff --git a/packages/evidence/src/object-storage.ts b/packages/evidence/src/object-storage.ts index 5099725..1d1f152 100644 --- a/packages/evidence/src/object-storage.ts +++ b/packages/evidence/src/object-storage.ts @@ -37,14 +37,20 @@ function hmac(key: string | Buffer, value: string) { return createHmac("sha256", key).update(value).digest(); } -function objectUrl(endpoint: string, bucket: string, storageKey: string) { +function bucketUrl(endpoint: string, bucket: string) { const url = new URL(endpoint); const basePath = url.pathname.replace(/\/$/, ""); + url.pathname = `${basePath}/${encodeURIComponent(bucket)}`; + return url; +} + +function objectUrl(endpoint: string, bucket: string, storageKey: string) { + const url = bucketUrl(endpoint, bucket); const encodedKey = storageKey .split("/") .map((part) => encodeURIComponent(part)) .join("/"); - url.pathname = `${basePath}/${encodeURIComponent(bucket)}/${encodedKey}`; + url.pathname = `${url.pathname}/${encodedKey}`; return url; } @@ -119,6 +125,30 @@ function storageConfiguration() { }; } +export async function checkObjectStorage(signal?: AbortSignal) { + const { endpoint, bucket, region, accessKey, secretKey } = + storageConfiguration(); + const url = bucketUrl(endpoint, bucket); + const emptyBody = new Uint8Array(); + const response = await fetch(url, { + method: "HEAD", + headers: signingHeaders( + "HEAD", + url, + emptyBody, + region, + accessKey, + secretKey, + ), + ...(signal ? { signal } : {}), + }); + if (!response.ok) { + throw new Error( + `Object storage rejected readiness check with ${response.status}`, + ); + } +} + export const defaultObjectStorage: CleanupObjectStorage = { async putObject(object) { const { endpoint, bucket, region, accessKey, secretKey } = From eb3707981327229f90ea5820af27b524d32df83d Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 12:55:10 +1000 Subject: [PATCH 18/47] fix(release): preserve homelab upgrade settings --- scripts/install-homelab.sh | 74 +++++++++++++++------------ tests/homelab/install-homelab.test.sh | 49 ++++++++++++++++-- 2 files changed, 86 insertions(+), 37 deletions(-) mode change 100644 => 100755 scripts/install-homelab.sh diff --git a/scripts/install-homelab.sh b/scripts/install-homelab.sh old mode 100644 new mode 100755 index fe8770b..11e732e --- a/scripts/install-homelab.sh +++ b/scripts/install-homelab.sh @@ -6,8 +6,8 @@ cd "$(dirname "$0")/.." env_file=".env.homelab" compose_file="deploy/docker/docker-compose.homelab.yml" -requested_public_url="${MUSTER_PUBLIC_URL:-http://muster.example.lan:3004}" -requested_http_port="${MUSTER_HTTP_PORT:-3004}" +default_public_url="http://muster.example.lan:3004" +default_http_port="3004" requested_version="${MUSTER_VERSION:-}" requested_image="${MUSTER_IMAGE:-}" @@ -55,44 +55,54 @@ upsert_env() { mv "$temporary_file" "$env_file" } -credentials_created=false +environment_created=false +admin_password_created=false if [[ ! -f "$env_file" ]]; then cp deploy/docker/.env.homelab.example "$env_file" - postgres_password="$(openssl rand -hex 24)" - auth_secret="$(openssl rand -hex 32)" - storage_secret="$(openssl rand -hex 24)" - connector_encryption_key="$(openssl rand -hex 32)" - agent_gateway_token="$(openssl rand -hex 32)" - admin_password="Muster!$(openssl rand -hex 12)" - - sed -i.bak \ - -e "s|MUSTER_PUBLIC_URL=.*|MUSTER_PUBLIC_URL=${requested_public_url}|" \ - -e "s|MUSTER_HTTP_PORT=.*|MUSTER_HTTP_PORT=${requested_http_port}|" \ - -e "s|MUSTER_VERSION=.*|MUSTER_VERSION=${requested_version}|" \ - -e "s|AUTH_TRUSTED_ORIGINS=.*|AUTH_TRUSTED_ORIGINS=${AUTH_TRUSTED_ORIGINS:-${requested_public_url},http://homelab:${requested_http_port}}|" \ - -e "s|generate-postgres-password|${postgres_password}|" \ - -e "s|generate-better-auth-secret|${auth_secret}|" \ - -e "s|generate-object-storage-secret|${storage_secret}|" \ - -e "s|generate-connector-encryption-key|${connector_encryption_key}|" \ - -e "s|generate-agent-gateway-token|${agent_gateway_token}|" \ - "$env_file" - rm "$env_file.bak" - { - printf 'MUSTER_LOCAL_ADMIN_EMAIL=%s\n' \ - "${MUSTER_LOCAL_ADMIN_EMAIL:-admin@muster.local}" - printf 'MUSTER_LOCAL_ADMIN_PASSWORD=%s\n' "$admin_password" - } >> "$env_file" - chmod 600 "$env_file" - credentials_created=true + environment_created=true + upsert_env POSTGRES_PASSWORD "$(openssl rand -hex 24)" + upsert_env BETTER_AUTH_SECRET "$(openssl rand -hex 32)" + upsert_env OBJECT_STORAGE_SECRET_KEY "$(openssl rand -hex 24)" + upsert_env CONNECTOR_ENCRYPTION_KEY "$(openssl rand -hex 32)" + upsert_env MUSTER_LOCAL_ADMIN_EMAIL "${MUSTER_LOCAL_ADMIN_EMAIL:-admin@muster.local}" + upsert_env MUSTER_LOCAL_ADMIN_PASSWORD "Muster!$(openssl rand -hex 12)" + admin_password_created=true fi if [[ -z "$(env_value MUSTER_LOCAL_ADMIN_EMAIL)" ]]; then upsert_env MUSTER_LOCAL_ADMIN_EMAIL "${MUSTER_LOCAL_ADMIN_EMAIL:-admin@muster.local}" - credentials_created=true fi if [[ -z "$(env_value MUSTER_LOCAL_ADMIN_PASSWORD)" ]]; then upsert_env MUSTER_LOCAL_ADMIN_PASSWORD "Muster!$(openssl rand -hex 12)" - credentials_created=true + admin_password_created=true +fi + +existing_public_url="$(env_value MUSTER_PUBLIC_URL)" +existing_http_port="$(env_value MUSTER_HTTP_PORT)" +existing_origins="$(env_value AUTH_TRUSTED_ORIGINS)" + +if [[ "${MUSTER_PUBLIC_URL+x}" == "x" ]]; then + requested_public_url="$MUSTER_PUBLIC_URL" +elif [[ "$environment_created" == "false" && -n "$existing_public_url" ]]; then + requested_public_url="$existing_public_url" +else + requested_public_url="$default_public_url" +fi + +if [[ "${MUSTER_HTTP_PORT+x}" == "x" ]]; then + requested_http_port="$MUSTER_HTTP_PORT" +elif [[ "$environment_created" == "false" && -n "$existing_http_port" ]]; then + requested_http_port="$existing_http_port" +else + requested_http_port="$default_http_port" +fi + +if [[ "${AUTH_TRUSTED_ORIGINS+x}" == "x" ]]; then + requested_origins="$AUTH_TRUSTED_ORIGINS" +elif [[ "$environment_created" == "false" && -n "$existing_origins" ]]; then + requested_origins="$existing_origins" +else + requested_origins="${requested_public_url},http://homelab:${requested_http_port}" fi if ! grep -q '^MUSTER_AGENT_GATEWAY_TOKEN=' "$env_file"; then @@ -223,6 +233,6 @@ else printf '%s\n' \ "Codex authentication: pending. Run docker compose --env-file .env.homelab -f ${compose_file} --profile setup run --rm codex-login." fi -if [[ "$credentials_created" == "true" ]]; then +if [[ "$admin_password_created" == "true" ]]; then printf 'New private homelab administrator password: %s\n' "$admin_password" fi diff --git a/tests/homelab/install-homelab.test.sh b/tests/homelab/install-homelab.test.sh index f689891..3336607 100755 --- a/tests/homelab/install-homelab.test.sh +++ b/tests/homelab/install-homelab.test.sh @@ -12,6 +12,7 @@ cp -R "$root/scripts" "$fixture/scripts" export TEST_LOG="$temporary_directory/commands.log" export PATH="$temporary_directory/bin:$PATH" +unset MUSTER_PUBLIC_URL MUSTER_HTTP_PORT AUTH_TRUSTED_ORIGINS cat > "$temporary_directory/bin/docker" <<'EOF' #!/usr/bin/env bash @@ -37,6 +38,7 @@ chmod +x "$temporary_directory/bin/docker" \ immutable_tag="sha-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" next_immutable_tag="sha-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +override_immutable_tag="sha-cccccccccccccccccccccccccccccccccccccccc" if ( cd "$fixture" @@ -62,21 +64,58 @@ grep -q -- '--env-file .env.homelab -f deploy/docker/docker-compose.homelab.yml grep -q '/api/auth/sign-up/email' "$TEST_LOG" grep -q 'New private homelab administrator password:' <<<"$first_output" grep -q 'Codex authentication: pending.' <<<"$first_output" +existing_admin_password="$( + sed -n 's/^MUSTER_LOCAL_ADMIN_PASSWORD=//p' "$fixture/.env.homelab" +)" second_output="$( cd "$fixture" - MUSTER_VERSION="$next_immutable_tag" \ - MUSTER_PUBLIC_URL="http://192.168.1.20:3004" \ - MUSTER_HTTP_PORT=3004 \ - bash ./scripts/install-homelab.sh + MUSTER_VERSION="$next_immutable_tag" bash ./scripts/install-homelab.sh )" grep -qx "MUSTER_VERSION=${next_immutable_tag}" "$fixture/.env.homelab" -grep -qx 'AUTH_TRUSTED_ORIGINS=http://192.168.1.20:3004,http://homelab:3004' \ +grep -qx 'MUSTER_PUBLIC_URL=http://192.168.1.19:3004' "$fixture/.env.homelab" +grep -qx 'MUSTER_HTTP_PORT=3004' "$fixture/.env.homelab" +grep -qx 'AUTH_TRUSTED_ORIGINS=http://192.168.1.19:3004,http://homelab:3004' \ "$fixture/.env.homelab" if grep -q 'New private homelab administrator password:' <<<"$second_output"; then printf 'installer reprinted an existing administrator password\n' >&2 exit 1 fi +if grep -Fq "$existing_admin_password" <<<"$second_output"; then + printf 'installer leaked an existing administrator password\n' >&2 + exit 1 +fi + +sed -i.bak '/^MUSTER_LOCAL_ADMIN_EMAIL=/d' "$fixture/.env.homelab" +rm "$fixture/.env.homelab.bak" +partial_output="$( + cd "$fixture" + MUSTER_VERSION="$next_immutable_tag" bash ./scripts/install-homelab.sh +)" + +grep -qx 'MUSTER_LOCAL_ADMIN_EMAIL=admin@muster.local' "$fixture/.env.homelab" +grep -qx "MUSTER_LOCAL_ADMIN_PASSWORD=${existing_admin_password}" \ + "$fixture/.env.homelab" +if grep -q 'New private homelab administrator password:' <<<"$partial_output" || + grep -Fq "$existing_admin_password" <<<"$partial_output"; then + printf 'email repair printed an existing administrator password\n' >&2 + exit 1 +fi + +( + cd "$fixture" + MUSTER_VERSION="$override_immutable_tag" \ + MUSTER_PUBLIC_URL="http://192.168.1.20:3014" \ + MUSTER_HTTP_PORT=3014 \ + AUTH_TRUSTED_ORIGINS="http://192.168.1.20:3014,http://homelab:3014" \ + bash ./scripts/install-homelab.sh >/dev/null +) + +grep -qx "MUSTER_VERSION=${override_immutable_tag}" "$fixture/.env.homelab" +grep -qx 'MUSTER_PUBLIC_URL=http://192.168.1.20:3014' "$fixture/.env.homelab" +grep -qx 'MUSTER_HTTP_PORT=3014' "$fixture/.env.homelab" +grep -qx 'AUTH_TRUSTED_ORIGINS=http://192.168.1.20:3014,http://homelab:3014' \ + "$fixture/.env.homelab" printf 'install-homelab test passed\n' From f7603186aed62ed5b8b0bfb16bf6949b2ce55a6d Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 12:55:14 +1000 Subject: [PATCH 19/47] ci(release): gate normalized amd64 publication --- .github/workflows/ci.yml | 42 ++++++------ docs/operations/release-homelab.md | 11 ++++ package.json | 1 + scripts/verify-image-platform.sh | 19 ++++++ tests/homelab/verify-image-platform.test.sh | 72 +++++++++++++++++++++ 5 files changed, 122 insertions(+), 23 deletions(-) create mode 100755 scripts/verify-image-platform.sh create mode 100755 tests/homelab/verify-image-platform.test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c373c8..a795b00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,7 @@ jobs: mc anonymous set none ci/muster-evidence' - run: pnpm install --frozen-lockfile - run: pnpm test:release-homelab + - run: pnpm test:release-image - name: Build database dependencies run: pnpm exec turbo build --filter=@muster/database - run: pnpm db:migrate @@ -105,6 +106,7 @@ jobs: retention-days: 14 container: + needs: quality runs-on: ubuntu-24.04 permissions: contents: read @@ -113,10 +115,13 @@ jobs: id-token: write env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - name: Normalize image reference + id: image + shell: bash + run: printf 'ref=%s/%s\n' "$REGISTRY" "${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" - name: Log in to GitHub Container Registry if: github.event_name == 'push' uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 @@ -128,7 +133,7 @@ jobs: id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ steps.image.outputs.ref }} tags: | type=raw,value=latest,enable={{is_default_branch}} type=ref,event=tag @@ -156,12 +161,12 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy:0.67.2 image \ --exit-code 1 --ignore-unfixed --severity HIGH,CRITICAL \ - "${REGISTRY}/${IMAGE_NAME}:sha-${GITHUB_SHA}" + "${{ steps.image.outputs.ref }}:sha-${GITHUB_SHA}" - name: Generate release SBOM if: github.event_name == 'push' uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 with: - image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} + image: ${{ steps.image.outputs.ref }}@${{ steps.build.outputs.digest }} format: cyclonedx-json output-file: muster-sbom.cdx.json upload-artifact: false @@ -169,7 +174,7 @@ jobs: if: github.event_name == 'pull_request' uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 with: - image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} + image: ${{ steps.image.outputs.ref }}:sha-${{ github.sha }} format: cyclonedx-json output-file: muster-sbom.cdx.json upload-artifact: false @@ -177,7 +182,7 @@ jobs: if: github.event_name == 'push' run: | printf '%s\n' \ - "${REGISTRY}/${IMAGE_NAME}@${{ steps.build.outputs.digest }}" > muster-image.txt + "${{ steps.image.outputs.ref }}@${{ steps.build.outputs.digest }}" > muster-image.txt sha256sum muster-sbom.cdx.json muster-image.txt > SHA256SUMS - name: Upload release evidence if: github.event_name == 'push' @@ -190,22 +195,19 @@ jobs: SHA256SUMS - name: Verify container starts if: github.event_name == 'pull_request' - shell: bash - run: | - image_ref="${REGISTRY}/${IMAGE_NAME,,}:sha-${GITHUB_SHA::7}" - docker image inspect "$image_ref" - - name: Attest published image + run: docker image inspect "${{ steps.image.outputs.ref }}:sha-${GITHUB_SHA}" + - name: Attest published image provenance if: github.event_name == 'push' uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4 with: - subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-name: ${{ steps.image.outputs.ref }} subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true - name: Attest published image SBOM if: github.event_name == 'push' uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4 with: - subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-name: ${{ steps.image.outputs.ref }} subject-digest: ${{ steps.build.outputs.digest }} sbom-path: muster-sbom.cdx.json push-to-registry: true @@ -213,24 +215,18 @@ jobs: if: github.event_name == 'push' shell: bash run: | - public_image="${REGISTRY}/${IMAGE_NAME,,}:sha-${GITHUB_SHA}" + public_image="${{ steps.image.outputs.ref }}:sha-${GITHUB_SHA}" docker logout "$REGISTRY" || true docker pull "$public_image" docker image inspect "$public_image" \ --format '{{.Os}}/{{.Architecture}}' | grep -x 'linux/amd64' - manifest="$(docker buildx imagetools inspect "$public_image" --raw)" - if jq -e '.manifests' >/dev/null <<<"$manifest"; then - jq -e ' - .manifests | length == 1 and - .[0].platform.os == "linux" and - .[0].platform.architecture == "amd64" - ' >/dev/null <<<"$manifest" - fi + docker buildx imagetools inspect "$public_image" --raw | + ./scripts/verify-image-platform.sh - name: Publication summary if: github.event_name == 'push' run: | { echo "### Published container" - echo "\`${REGISTRY}/${IMAGE_NAME}\`" + echo "\`${{ steps.image.outputs.ref }}\`" echo "The workflow verified an anonymous pull after publication. GHCR visibility is configured once at the package level and retained by subsequent releases." } >> "$GITHUB_STEP_SUMMARY" diff --git a/docs/operations/release-homelab.md b/docs/operations/release-homelab.md index 85661f8..6c722df 100644 --- a/docs/operations/release-homelab.md +++ b/docs/operations/release-homelab.md @@ -6,6 +6,13 @@ the published full `sha-<40-hex-commit>` tag, OCI digest, SBOM, provenance, and `linux/amd64` application image, scans it with Trivy, and verifies an anonymous GHCR pull after logout. +The container job has an explicit `needs: quality` dependency, so a push cannot +publish before lint, typecheck, unit, build, migration, and Chromium E2E pass. +GitHub Actions cannot express `needs` across independent workflow files; the +separate security workflow therefore remains a required same-commit +branch-protection and deployment gate. Do not deploy an image until that +security run is also green. + Before the first release, make the GitHub Container Registry package public in its package settings. CI deliberately logs out before pulling; it fails until anonymous users can pull the package. Do not treat a successful authenticated @@ -35,6 +42,10 @@ activity. Keep `MUSTER_PUBLIC_URL` and `AUTH_TRUSTED_ORIGINS` limited to exact IP and `homelab` browser origins; pass both variables explicitly if deployment uses a different approved pair. +Upgrade and rollback preserve the existing public URL, HTTP port, and trusted +origins unless each value is explicitly supplied. This prevents an immutable +tag change from silently resetting a non-default homelab address. + The private `codex-state` volume survives application upgrades and rollback. Authenticate it only through the setup profile; never copy, publish, or log `auth.json`: diff --git a/package.json b/package.json index 5cd5ce9..1cb5564 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "test:e2e:homelab": "playwright test --config=playwright.homelab.config.ts", "test:homelab-installer": "bash scripts/test-install-homelab.sh", "test:release-homelab": "bash tests/homelab/install-homelab.test.sh", + "test:release-image": "bash tests/homelab/verify-image-platform.test.sh", "contracts:generate": "pnpm --filter @muster/contracts generate", "db:generate": "pnpm --filter @muster/database generate", "db:migrate": "pnpm --filter @muster/database migrate", diff --git a/scripts/verify-image-platform.sh b/scripts/verify-image-platform.sh new file mode 100755 index 0000000..5ab1bf9 --- /dev/null +++ b/scripts/verify-image-platform.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +jq -e ' + [ + .manifests[] + | select( + .mediaType == "application/vnd.oci.image.manifest.v1+json" + or .mediaType == "application/vnd.docker.distribution.manifest.v2+json" + ) + | select( + (.annotations["vnd.docker.reference.type"] // "") + != "attestation-manifest" + ) + ] as $application_manifests + | $application_manifests | length == 1 + and $application_manifests[0].platform.os == "linux" + and $application_manifests[0].platform.architecture == "amd64" +' >/dev/null diff --git a/tests/homelab/verify-image-platform.test.sh b/tests/homelab/verify-image-platform.test.sh new file mode 100755 index 0000000..73b2143 --- /dev/null +++ b/tests/homelab/verify-image-platform.test.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "$0")/../.." && pwd)" +verify="$root/scripts/verify-image-platform.sh" + +"$verify" <<'JSON' +{ + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:application", + "platform": { "os": "linux", "architecture": "amd64" } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:provenance", + "platform": { "os": "unknown", "architecture": "unknown" }, + "annotations": { + "vnd.docker.reference.type": "attestation-manifest" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:sbom", + "platform": { "os": "unknown", "architecture": "unknown" }, + "annotations": { + "vnd.docker.reference.type": "attestation-manifest" + } + } + ] +} +JSON + +if "$verify" <<'JSON' +{ + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "platform": { "os": "linux", "architecture": "amd64" } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "platform": { "os": "linux", "architecture": "arm64" } + } + ] +} +JSON +then + printf 'platform verifier accepted multiple application manifests\n' >&2 + exit 1 +fi + +if "$verify" <<'JSON' +{ + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "platform": { "os": "linux", "architecture": "arm64" } + } + ] +} +JSON +then + printf 'platform verifier accepted a non-amd64 application manifest\n' >&2 + exit 1 +fi + +printf 'verify-image-platform test passed\n' From b2f3b28d1ed304d4fbeecb24d5f284f84406dcb9 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 13:01:57 +1000 Subject: [PATCH 20/47] fix(release): bind attestations to amd64 manifest --- scripts/verify-image-platform.sh | 37 ++++--- tests/homelab/verify-image-platform.test.sh | 110 +++++++++++++++++++- 2 files changed, 133 insertions(+), 14 deletions(-) diff --git a/scripts/verify-image-platform.sh b/scripts/verify-image-platform.sh index 5ab1bf9..6b56107 100755 --- a/scripts/verify-image-platform.sh +++ b/scripts/verify-image-platform.sh @@ -2,18 +2,31 @@ set -euo pipefail jq -e ' - [ - .manifests[] - | select( - .mediaType == "application/vnd.oci.image.manifest.v1+json" - or .mediaType == "application/vnd.docker.distribution.manifest.v2+json" - ) - | select( - (.annotations["vnd.docker.reference.type"] // "") - != "attestation-manifest" - ) - ] as $application_manifests - | $application_manifests | length == 1 + def image_manifest: + .mediaType == "application/vnd.oci.image.manifest.v1+json" + or .mediaType == "application/vnd.docker.distribution.manifest.v2+json"; + def attestation: + image_manifest + and .annotations["vnd.docker.reference.type"] == "attestation-manifest"; + + .manifests as $manifests + | [$manifests[] | select(attestation | not)] as $application_manifests + | [$manifests[] | select(attestation)] as $attestation_manifests + | ($application_manifests | length) == 1 + and ($attestation_manifests | length) >= 1 + and ($manifests | length) + == (($application_manifests | length) + ($attestation_manifests | length)) + and ($application_manifests[0] | image_manifest) + and ($application_manifests[0].digest | startswith("sha256:")) and $application_manifests[0].platform.os == "linux" and $application_manifests[0].platform.architecture == "amd64" + and ( + $attestation_manifests + | all( + .platform.os == "unknown" + and .platform.architecture == "unknown" + and .annotations["vnd.docker.reference.digest"] + == $application_manifests[0].digest + ) + ) ' >/dev/null diff --git a/tests/homelab/verify-image-platform.test.sh b/tests/homelab/verify-image-platform.test.sh index 73b2143..55d5a7d 100755 --- a/tests/homelab/verify-image-platform.test.sh +++ b/tests/homelab/verify-image-platform.test.sh @@ -18,7 +18,8 @@ verify="$root/scripts/verify-image-platform.sh" "digest": "sha256:provenance", "platform": { "os": "unknown", "architecture": "unknown" }, "annotations": { - "vnd.docker.reference.type": "attestation-manifest" + "vnd.docker.reference.type": "attestation-manifest", + "vnd.docker.reference.digest": "sha256:application" } }, { @@ -26,7 +27,8 @@ verify="$root/scripts/verify-image-platform.sh" "digest": "sha256:sbom", "platform": { "os": "unknown", "architecture": "unknown" }, "annotations": { - "vnd.docker.reference.type": "attestation-manifest" + "vnd.docker.reference.type": "attestation-manifest", + "vnd.docker.reference.digest": "sha256:application" } } ] @@ -39,11 +41,22 @@ if "$verify" <<'JSON' "manifests": [ { "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:amd64", "platform": { "os": "linux", "architecture": "amd64" } }, { "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:arm64", "platform": { "os": "linux", "architecture": "arm64" } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:attestation", + "platform": { "os": "unknown", "architecture": "unknown" }, + "annotations": { + "vnd.docker.reference.type": "attestation-manifest", + "vnd.docker.reference.digest": "sha256:amd64" + } } ] } @@ -59,7 +72,17 @@ if "$verify" <<'JSON' "manifests": [ { "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:arm64", "platform": { "os": "linux", "architecture": "arm64" } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:attestation", + "platform": { "os": "unknown", "architecture": "unknown" }, + "annotations": { + "vnd.docker.reference.type": "attestation-manifest", + "vnd.docker.reference.digest": "sha256:arm64" + } } ] } @@ -69,4 +92,87 @@ then exit 1 fi +if "$verify" <<'JSON' +{ + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:application", + "platform": { "os": "linux", "architecture": "amd64" } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:malicious-arm64", + "platform": { "os": "linux", "architecture": "arm64" }, + "annotations": { + "vnd.docker.reference.type": "attestation-manifest", + "vnd.docker.reference.digest": "sha256:application" + } + } + ] +} +JSON +then + printf 'platform verifier trusted an arm64 attestation label\n' >&2 + exit 1 +fi + +if "$verify" <<'JSON' +{ + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:application", + "platform": { "os": "linux", "architecture": "amd64" } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:mismatched-attestation", + "platform": { "os": "unknown", "architecture": "unknown" }, + "annotations": { + "vnd.docker.reference.type": "attestation-manifest", + "vnd.docker.reference.digest": "sha256:different-application" + } + } + ] +} +JSON +then + printf 'platform verifier accepted an unrelated attestation manifest\n' >&2 + exit 1 +fi + +if "$verify" <<'JSON' +{ + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:application", + "platform": { "os": "linux", "architecture": "amd64" } + }, + { + "mediaType": "application/vnd.oci.artifact.manifest.v1+json", + "digest": "sha256:unexpected", + "artifactType": "application/example" + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:attestation", + "platform": { "os": "unknown", "architecture": "unknown" }, + "annotations": { + "vnd.docker.reference.type": "attestation-manifest", + "vnd.docker.reference.digest": "sha256:application" + } + } + ] +} +JSON +then + printf 'platform verifier accepted an unexpected descriptor\n' >&2 + exit 1 +fi + printf 'verify-image-platform test passed\n' From bfc790a411d1b89c3d7f81eda2c7127278456391 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 13:02:00 +1000 Subject: [PATCH 21/47] ci(release): promote only verified secure digests --- .github/workflows/ci.yml | 102 ++++++++++++++++++++++------- .github/workflows/security.yml | 4 +- docs/operations/release-homelab.md | 13 ++-- 3 files changed, 85 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a795b00..eb4e723 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,9 +105,18 @@ jobs: if-no-files-found: ignore retention-days: 14 + release-security: + permissions: + contents: read + security-events: write + uses: ./.github/workflows/security.yml + container: needs: quality runs-on: ubuntu-24.04 + outputs: + image_ref: ${{ steps.image.outputs.ref }} + image_digest: ${{ steps.build.outputs.digest }} permissions: contents: read packages: write @@ -121,7 +130,17 @@ jobs: - name: Normalize image reference id: image shell: bash - run: printf 'ref=%s/%s\n' "$REGISTRY" "${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" + run: | + image_ref="${REGISTRY}/${GITHUB_REPOSITORY,,}" + if [[ "$GITHUB_EVENT_NAME" == "push" ]]; then + build_ref="${image_ref}:staging-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + else + build_ref="${image_ref}:verify-${GITHUB_SHA}" + fi + { + printf 'ref=%s\n' "$image_ref" + printf 'build_ref=%s\n' "$build_ref" + } >> "$GITHUB_OUTPUT" - name: Log in to GitHub Container Registry if: github.event_name == 'push' uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 @@ -129,19 +148,6 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Generate image tags and OCI labels - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 - with: - images: ${{ steps.image.outputs.ref }} - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=ref,event=tag - type=sha,format=long - labels: | - org.opencontainers.image.title=Muster - org.opencontainers.image.description=Shared workspace for human and agent-driven security operations - org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} - uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 id: build with: @@ -149,10 +155,13 @@ jobs: push: ${{ github.event_name == 'push' }} load: ${{ github.event_name == 'pull_request' }} platforms: linux/amd64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - provenance: ${{ github.event_name == 'push' && 'mode=max' || 'false' }} - sbom: ${{ github.event_name == 'push' }} + tags: ${{ steps.image.outputs.build_ref }} + labels: | + org.opencontainers.image.title=Muster + org.opencontainers.image.description=Shared workspace for human and agent-driven security operations + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + provenance: mode=max + sbom: true cache-from: type=gha cache-to: type=gha,mode=max - name: Scan built image with Trivy @@ -161,7 +170,7 @@ jobs: --volume /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy:0.67.2 image \ --exit-code 1 --ignore-unfixed --severity HIGH,CRITICAL \ - "${{ steps.image.outputs.ref }}:sha-${GITHUB_SHA}" + "${{ steps.image.outputs.build_ref }}" - name: Generate release SBOM if: github.event_name == 'push' uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 @@ -174,10 +183,16 @@ jobs: if: github.event_name == 'pull_request' uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 with: - image: ${{ steps.image.outputs.ref }}:sha-${{ github.sha }} + image: ${{ steps.image.outputs.build_ref }} format: cyclonedx-json output-file: muster-sbom.cdx.json upload-artifact: false + - name: Verify staged OCI application platform + if: github.event_name == 'push' + run: | + docker buildx imagetools inspect \ + "${{ steps.image.outputs.ref }}@${{ steps.build.outputs.digest }}" --raw | + ./scripts/verify-image-platform.sh - name: Record immutable image evidence and checksums if: github.event_name == 'push' run: | @@ -195,7 +210,7 @@ jobs: SHA256SUMS - name: Verify container starts if: github.event_name == 'pull_request' - run: docker image inspect "${{ steps.image.outputs.ref }}:sha-${GITHUB_SHA}" + run: docker image inspect "${{ steps.image.outputs.build_ref }}" - name: Attest published image provenance if: github.event_name == 'push' uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4 @@ -211,11 +226,49 @@ jobs: subject-digest: ${{ steps.build.outputs.digest }} sbom-path: muster-sbom.cdx.json push-to-registry: true + + promote: + if: github.event_name == 'push' + needs: [container, release-security] + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + env: + REGISTRY: ghcr.io + IMAGE_REF: ${{ needs.container.outputs.image_ref }} + IMAGE_DIGEST: ${{ needs.container.outputs.image_digest }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - name: Generate release tags + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 + with: + images: ${{ env.IMAGE_REF }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=tag + type=sha,format=long + - name: Log in to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Promote verified digest to release tags + env: + RELEASE_TAGS: ${{ steps.meta.outputs.tags }} + run: | + while IFS= read -r tag; do + [[ -n "$tag" ]] || continue + docker buildx imagetools create \ + --tag "$tag" "${IMAGE_REF}@${IMAGE_DIGEST}" + done <<< "$RELEASE_TAGS" - name: Verify anonymous public pull - if: github.event_name == 'push' shell: bash run: | - public_image="${{ steps.image.outputs.ref }}:sha-${GITHUB_SHA}" + public_image="${IMAGE_REF}:sha-${GITHUB_SHA}" docker logout "$REGISTRY" || true docker pull "$public_image" docker image inspect "$public_image" \ @@ -223,10 +276,9 @@ jobs: docker buildx imagetools inspect "$public_image" --raw | ./scripts/verify-image-platform.sh - name: Publication summary - if: github.event_name == 'push' run: | { echo "### Published container" - echo "\`${{ steps.image.outputs.ref }}\`" + echo "\`${IMAGE_REF}@${IMAGE_DIGEST}\`" echo "The workflow verified an anonymous pull after publication. GHCR visibility is configured once at the package level and retained by subsequent releases." } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index d4217f6..eb8a06e 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,9 +1,7 @@ name: Security and supply chain on: - push: - branches: [main] - pull_request: + workflow_call: schedule: - cron: "17 3 * * 1" diff --git a/docs/operations/release-homelab.md b/docs/operations/release-homelab.md index 6c722df..783a6aa 100644 --- a/docs/operations/release-homelab.md +++ b/docs/operations/release-homelab.md @@ -6,12 +6,13 @@ the published full `sha-<40-hex-commit>` tag, OCI digest, SBOM, provenance, and `linux/amd64` application image, scans it with Trivy, and verifies an anonymous GHCR pull after logout. -The container job has an explicit `needs: quality` dependency, so a push cannot -publish before lint, typecheck, unit, build, migration, and Chromium E2E pass. -GitHub Actions cannot express `needs` across independent workflow files; the -separate security workflow therefore remains a required same-commit -branch-protection and deployment gate. Do not deploy an image until that -security run is also green. +The container job has an explicit `needs: quality` dependency. It pushes only a +run-scoped staging tag, then performs Trivy, SBOM, checksum, attestation, and OCI +platform verification against that digest. The security workflow is reusable: +CI invokes it for the same commit, while its own weekly schedule remains +available. A separate promotion job needs both the verified container and +same-commit security jobs before creating `latest`, version, or SHA release +tags. Before the first release, make the GitHub Container Registry package public in its package settings. CI deliberately logs out before pulling; it fails until From 97503ae440679e8e436f4bfb27c431deda198022 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 13:10:55 +1000 Subject: [PATCH 22/47] ci(release): protect immutable image promotion --- .github/workflows/ci.yml | 45 ++++++++---- docs/operations/release-homelab.md | 15 ++-- package.json | 2 +- scripts/promote-image-tag.sh | 74 ++++++++++++++++++++ tests/homelab/promote-image-tag.test.sh | 91 +++++++++++++++++++++++++ 5 files changed, 209 insertions(+), 18 deletions(-) create mode 100755 scripts/promote-image-tag.sh create mode 100755 tests/homelab/promote-image-tag.test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb4e723..7146392 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -226,11 +226,25 @@ jobs: subject-digest: ${{ steps.build.outputs.digest }} sbom-path: muster-sbom.cdx.json push-to-registry: true + - name: Verify anonymous staging pull + if: github.event_name == 'push' + shell: bash + run: | + public_image="${{ steps.image.outputs.build_ref }}" + docker logout "$REGISTRY" || true + docker pull "$public_image" + docker image inspect "$public_image" \ + --format '{{.Os}}/{{.Architecture}}' | grep -x 'linux/amd64' + docker buildx imagetools inspect "$public_image" --raw | + ./scripts/verify-image-platform.sh promote: if: github.event_name == 'push' needs: [container, release-security] runs-on: ubuntu-24.04 + concurrency: + group: muster-release-tags-${{ github.repository }} + cancel-in-progress: false permissions: contents: read packages: write @@ -260,21 +274,28 @@ jobs: env: RELEASE_TAGS: ${{ steps.meta.outputs.tags }} run: | + promotion_policy() { + case "${1##*:}" in + latest) printf 'movable\n' ;; + "sha-${GITHUB_SHA}" | v*) printf 'immutable\n' ;; + *) + printf 'Unexpected release tag: %s\n' "$1" >&2 + return 1 + ;; + esac + } while IFS= read -r tag; do [[ -n "$tag" ]] || continue - docker buildx imagetools create \ - --tag "$tag" "${IMAGE_REF}@${IMAGE_DIGEST}" + policy="$(promotion_policy "$tag")" + ./scripts/promote-image-tag.sh \ + "$tag" "${IMAGE_REF}@${IMAGE_DIGEST}" "$policy" check + done <<< "$RELEASE_TAGS" + while IFS= read -r tag; do + [[ -n "$tag" ]] || continue + policy="$(promotion_policy "$tag")" + ./scripts/promote-image-tag.sh \ + "$tag" "${IMAGE_REF}@${IMAGE_DIGEST}" "$policy" apply done <<< "$RELEASE_TAGS" - - name: Verify anonymous public pull - shell: bash - run: | - public_image="${IMAGE_REF}:sha-${GITHUB_SHA}" - docker logout "$REGISTRY" || true - docker pull "$public_image" - docker image inspect "$public_image" \ - --format '{{.Os}}/{{.Architecture}}' | grep -x 'linux/amd64' - docker buildx imagetools inspect "$public_image" --raw | - ./scripts/verify-image-platform.sh - name: Publication summary run: | { diff --git a/docs/operations/release-homelab.md b/docs/operations/release-homelab.md index 783a6aa..b082e2a 100644 --- a/docs/operations/release-homelab.md +++ b/docs/operations/release-homelab.md @@ -8,11 +8,16 @@ GHCR pull after logout. The container job has an explicit `needs: quality` dependency. It pushes only a run-scoped staging tag, then performs Trivy, SBOM, checksum, attestation, and OCI -platform verification against that digest. The security workflow is reusable: -CI invokes it for the same commit, while its own weekly schedule remains -available. A separate promotion job needs both the verified container and -same-commit security jobs before creating `latest`, version, or SHA release -tags. +platform verification against that digest. It also logs out and proves the +run-scoped staging tag is anonymously pullable before promotion. The security +workflow is reusable: CI invokes it for the same commit, while its own weekly +schedule remains available. A separate promotion job needs both the verified +container and same-commit security jobs before creating release tags. + +Full-SHA and version tags are write-once. Promotion creates either tag only +when absent, does nothing when it already resolves to the verified digest, and +fails when it resolves elsewhere. A serialized preflight checks every release +tag before mutation. Only `latest` may move to a newer verified digest. Before the first release, make the GitHub Container Registry package public in its package settings. CI deliberately logs out before pulling; it fails until diff --git a/package.json b/package.json index 1cb5564..3d2f477 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test:e2e:homelab": "playwright test --config=playwright.homelab.config.ts", "test:homelab-installer": "bash scripts/test-install-homelab.sh", "test:release-homelab": "bash tests/homelab/install-homelab.test.sh", - "test:release-image": "bash tests/homelab/verify-image-platform.test.sh", + "test:release-image": "bash tests/homelab/verify-image-platform.test.sh && bash tests/homelab/promote-image-tag.test.sh", "contracts:generate": "pnpm --filter @muster/contracts generate", "db:generate": "pnpm --filter @muster/database generate", "db:migrate": "pnpm --filter @muster/database migrate", diff --git a/scripts/promote-image-tag.sh b/scripts/promote-image-tag.sh new file mode 100755 index 0000000..ef483fc --- /dev/null +++ b/scripts/promote-image-tag.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +target_tag="${1:-}" +source_ref="${2:-}" +policy="${3:-immutable}" +operation="${4:-apply}" +source_digest="${source_ref##*@}" + +if [[ -z "$target_tag" || ! "$source_ref" =~ @sha256:[0-9a-f]{64}$ ]]; then + printf 'Usage: %s [immutable|movable] [check|apply]\n' \ + "$0" >&2 + exit 2 +fi +if [[ "$policy" != "immutable" && "$policy" != "movable" ]]; then + printf 'Promotion policy must be immutable or movable.\n' >&2 + exit 2 +fi +if [[ "$operation" != "check" && "$operation" != "apply" ]]; then + printf 'Promotion operation must be check or apply.\n' >&2 + exit 2 +fi + +inspect_tag() { + local tag="$1" + local error_file + local manifest + error_file="$(mktemp)" + if manifest="$( + docker buildx imagetools inspect "$tag" \ + --format '{{json .Manifest}}' 2>"$error_file" + )"; then + rm "$error_file" + jq -er '.digest' <<<"$manifest" + return 0 + fi + if grep -Eiq \ + 'manifest unknown|MANIFEST_UNKNOWN|name unknown|not found: manifest' \ + "$error_file"; then + rm "$error_file" + return 3 + fi + rm "$error_file" + printf 'Unable to determine current digest for %s.\n' "$tag" >&2 + return 1 +} + +existing_digest="" +inspect_status=0 +existing_digest="$(inspect_tag "$target_tag")" || inspect_status=$? + +if [[ "$inspect_status" == "0" && "$existing_digest" == "$source_digest" ]]; then + printf '%s already points to verified digest.\n' "$target_tag" + exit 0 +fi +if [[ "$policy" == "immutable" && "$inspect_status" == "0" ]]; then + printf 'Refusing to move immutable tag %s from %s to %s.\n' \ + "$target_tag" "$existing_digest" "$source_digest" >&2 + exit 1 +fi +if [[ "$inspect_status" != "0" && "$inspect_status" != "3" ]]; then + exit "$inspect_status" +fi +if [[ "$operation" == "check" ]]; then + exit 0 +fi + +docker buildx imagetools create --tag "$target_tag" "$source_ref" + +promoted_digest="$(inspect_tag "$target_tag")" +if [[ "$promoted_digest" != "$source_digest" ]]; then + printf 'Promotion verification failed for %s.\n' "$target_tag" >&2 + exit 1 +fi diff --git a/tests/homelab/promote-image-tag.test.sh b/tests/homelab/promote-image-tag.test.sh new file mode 100755 index 0000000..d93f7ec --- /dev/null +++ b/tests/homelab/promote-image-tag.test.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "$0")/../.." && pwd)" +temporary_directory="$(mktemp -d)" +trap 'rm -rf "$temporary_directory"' EXIT + +export PATH="$temporary_directory/bin:$PATH" +export PROMOTE_TEST_STATE="$temporary_directory/tags" +export PROMOTE_TEST_LOG="$temporary_directory/commands" +mkdir -p "$temporary_directory/bin" +touch "$PROMOTE_TEST_STATE" "$PROMOTE_TEST_LOG" + +cat > "$temporary_directory/bin/docker" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >> "$PROMOTE_TEST_LOG" + +if [[ "$*" == *"imagetools inspect"* ]]; then + tag="$4" + if [[ "$tag" == *":v9.9.9" ]]; then + printf 'registry transport unavailable\n' >&2 + exit 1 + fi + digest="$(awk -F '|' -v tag="$tag" '$1 == tag { print $2 }' "$PROMOTE_TEST_STATE")" + if [[ -z "$digest" ]]; then + printf 'manifest unknown: not found\n' >&2 + exit 1 + fi + printf '{"digest":"%s"}\n' "$digest" + exit 0 +fi + +if [[ "$*" == *"imagetools create"* ]]; then + tag="$5" + source_ref="$6" + digest="${source_ref##*@}" + awk -F '|' -v tag="$tag" '$1 != tag' "$PROMOTE_TEST_STATE" \ + > "${PROMOTE_TEST_STATE}.tmp" + printf '%s|%s\n' "$tag" "$digest" >> "${PROMOTE_TEST_STATE}.tmp" + mv "${PROMOTE_TEST_STATE}.tmp" "$PROMOTE_TEST_STATE" + exit 0 +fi + +printf 'unexpected docker invocation\n' >&2 +exit 1 +EOF +chmod +x "$temporary_directory/bin/docker" + +verified_digest="sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +different_digest="sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +source_ref="ghcr.io/example/muster@${verified_digest}" +sha_tag="ghcr.io/example/muster:sha-1111111111111111111111111111111111111111" +version_tag="ghcr.io/example/muster:v1.2.3" +unavailable_tag="ghcr.io/example/muster:v9.9.9" +latest_tag="ghcr.io/example/muster:latest" + +"$root/scripts/promote-image-tag.sh" "$sha_tag" "$source_ref" immutable check +if grep -q 'imagetools create' "$PROMOTE_TEST_LOG"; then + printf 'immutable preflight created a missing tag\n' >&2 + exit 1 +fi +"$root/scripts/promote-image-tag.sh" "$sha_tag" "$source_ref" immutable +grep -qx "${sha_tag}|${verified_digest}" "$PROMOTE_TEST_STATE" + +: > "$PROMOTE_TEST_LOG" +"$root/scripts/promote-image-tag.sh" "$sha_tag" "$source_ref" immutable +if grep -q 'imagetools create' "$PROMOTE_TEST_LOG"; then + printf 'matching immutable tag was rewritten\n' >&2 + exit 1 +fi + +printf '%s|%s\n' "$version_tag" "$different_digest" >> "$PROMOTE_TEST_STATE" +if "$root/scripts/promote-image-tag.sh" \ + "$version_tag" "$source_ref" immutable >/dev/null 2>&1; then + printf 'different immutable version tag was moved\n' >&2 + exit 1 +fi +grep -qx "${version_tag}|${different_digest}" "$PROMOTE_TEST_STATE" + +if "$root/scripts/promote-image-tag.sh" \ + "$unavailable_tag" "$source_ref" immutable >/dev/null 2>&1; then + printf 'registry transport failure was treated as a missing tag\n' >&2 + exit 1 +fi + +printf '%s|%s\n' "$latest_tag" "$different_digest" >> "$PROMOTE_TEST_STATE" +"$root/scripts/promote-image-tag.sh" "$latest_tag" "$source_ref" movable +grep -qx "${latest_tag}|${verified_digest}" "$PROMOTE_TEST_STATE" + +printf 'promote-image-tag test passed\n' From 752eda752f01a4c2f4d0a32222759dd107678ad9 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 13:18:07 +1000 Subject: [PATCH 23/47] fix(release): recognize missing buildx tags --- scripts/promote-image-tag.sh | 2 +- tests/homelab/promote-image-tag.test.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/promote-image-tag.sh b/scripts/promote-image-tag.sh index ef483fc..743ae4b 100755 --- a/scripts/promote-image-tag.sh +++ b/scripts/promote-image-tag.sh @@ -35,7 +35,7 @@ inspect_tag() { return 0 fi if grep -Eiq \ - 'manifest unknown|MANIFEST_UNKNOWN|name unknown|not found: manifest' \ + 'manifest unknown|MANIFEST_UNKNOWN|name unknown|not found: manifest|^ERROR: .*: not found$' \ "$error_file"; then rm "$error_file" return 3 diff --git a/tests/homelab/promote-image-tag.test.sh b/tests/homelab/promote-image-tag.test.sh index d93f7ec..f2cbacf 100755 --- a/tests/homelab/promote-image-tag.test.sh +++ b/tests/homelab/promote-image-tag.test.sh @@ -24,7 +24,7 @@ if [[ "$*" == *"imagetools inspect"* ]]; then fi digest="$(awk -F '|' -v tag="$tag" '$1 == tag { print $2 }' "$PROMOTE_TEST_STATE")" if [[ -z "$digest" ]]; then - printf 'manifest unknown: not found\n' >&2 + printf 'ERROR: %s: not found\n' "$tag" >&2 exit 1 fi printf '{"digest":"%s"}\n' "$digest" From 11c38233b867d936906ef823b181cbcdb1b8e27a Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 15:25:11 +1000 Subject: [PATCH 24/47] fix(release): publish immutable tags only --- .github/workflows/ci.yml | 2 -- scripts/install-homelab.sh | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7146392..d344f86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -261,7 +261,6 @@ jobs: with: images: ${{ env.IMAGE_REF }} tags: | - type=raw,value=latest,enable={{is_default_branch}} type=ref,event=tag type=sha,format=long - name: Log in to GitHub Container Registry @@ -276,7 +275,6 @@ jobs: run: | promotion_policy() { case "${1##*:}" in - latest) printf 'movable\n' ;; "sha-${GITHUB_SHA}" | v*) printf 'immutable\n' ;; *) printf 'Unexpected release tag: %s\n' "$1" >&2 diff --git a/scripts/install-homelab.sh b/scripts/install-homelab.sh index 11e732e..11b26c1 100755 --- a/scripts/install-homelab.sh +++ b/scripts/install-homelab.sh @@ -111,8 +111,8 @@ if ! grep -q '^MUSTER_AGENT_GATEWAY_TOKEN=' "$env_file"; then chmod 600 "$env_file" fi -# Persist an explicitly requested reviewed image reference before sourcing the env -# file. Otherwise the template's `latest` value would silently win. +# Persist an explicitly requested reviewed image reference before reading the +# environment file so a prior installation cannot silently override it. if [[ -n "$requested_version" ]]; then sed -i.bak \ -e "s|^MUSTER_VERSION=.*|MUSTER_VERSION=${requested_version}|" \ From 8826639f771c82d7aeaa5fefc5acd116b6daf639 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 11:03:24 +1000 Subject: [PATCH 25/47] feat: add governed Slack agent harness --- apps/agent-gateway/src/runtime.ts | 36 + .../api/v1/agent-harness/invocations/route.ts | 20 + .../api/v1/agent-harness/manifests/route.ts | 16 + .../api/v1/agent-harness/runs/[id]/route.ts | 37 + apps/web/app/api/v1/slack/commands/route.ts | 40 + apps/web/app/api/v1/slack/events/route.ts | 31 + apps/web/app/api/v1/slack/exposures/route.ts | 39 + apps/web/app/api/v1/slack/health/route.ts | 12 + apps/web/app/api/v1/slack/identities/route.ts | 23 + apps/web/app/api/v1/slack/install/route.ts | 29 + .../app/api/v1/slack/interactions/route.ts | 26 + .../app/api/v1/slack/oauth/callback/route.ts | 59 + apps/web/package.json | 1 + apps/worker/package.json | 1 + apps/worker/src/index.ts | 13 + docs/integrations/agent-harness.md | 59 + packages/agent-harness/package.json | 33 + packages/agent-harness/src/index.test.ts | 38 + packages/agent-harness/src/index.ts | 1106 +++++++++++++++++ packages/agent-harness/tsconfig.json | 9 + packages/contracts/src/index.ts | 62 + packages/database/src/schema.ts | 185 +++ packages/database/src/verify-clean-install.ts | 5 + 23 files changed, 1880 insertions(+) create mode 100644 apps/web/app/api/v1/agent-harness/invocations/route.ts create mode 100644 apps/web/app/api/v1/agent-harness/manifests/route.ts create mode 100644 apps/web/app/api/v1/agent-harness/runs/[id]/route.ts create mode 100644 apps/web/app/api/v1/slack/commands/route.ts create mode 100644 apps/web/app/api/v1/slack/events/route.ts create mode 100644 apps/web/app/api/v1/slack/exposures/route.ts create mode 100644 apps/web/app/api/v1/slack/health/route.ts create mode 100644 apps/web/app/api/v1/slack/identities/route.ts create mode 100644 apps/web/app/api/v1/slack/install/route.ts create mode 100644 apps/web/app/api/v1/slack/interactions/route.ts create mode 100644 apps/web/app/api/v1/slack/oauth/callback/route.ts create mode 100644 docs/integrations/agent-harness.md create mode 100644 packages/agent-harness/package.json create mode 100644 packages/agent-harness/src/index.test.ts create mode 100644 packages/agent-harness/src/index.ts create mode 100644 packages/agent-harness/tsconfig.json diff --git a/apps/agent-gateway/src/runtime.ts b/apps/agent-gateway/src/runtime.ts index 86f287b..a331563 100644 --- a/apps/agent-gateway/src/runtime.ts +++ b/apps/agent-gateway/src/runtime.ts @@ -565,6 +565,18 @@ export class DurableAgentRuntime { error: reason, }, ); + await writeOutbox(tx, { + organisationId: updated.organisationId, + eventType: "agent.run.settled", + aggregateType: "agent_run", + aggregateId: updated.id, + queueName: "muster-notifications", + payload: { runId: updated.id, status: "cancelled" }, + idempotencyKey: `agent.run.settled:${updated.id}`, + traceId: redactObservationText( + this.request(updated).traceId ?? `agent-run-${updated.id}`, + ), + }); const [hunt] = await tx .update(schema.huntRuns) .set({ @@ -1385,6 +1397,18 @@ export class DurableAgentRuntime { outputHash: result.outputHash, outputSchema: result.schemaName, }); + await writeOutbox(tx, { + organisationId: updated.organisationId, + eventType: "agent.run.settled", + aggregateType: "agent_run", + aggregateId: updated.id, + queueName: "muster-notifications", + payload: { runId: updated.id, status: "completed" }, + idempotencyKey: `agent.run.settled:${updated.id}`, + traceId: redactObservationText( + this.request(updated).traceId ?? `agent-run-${updated.id}`, + ), + }); const [hunt] = await tx .update(schema.huntRuns) .set({ @@ -1563,6 +1587,18 @@ export class DurableAgentRuntime { failureCode: failure.code, error: failure.message, }); + await writeOutbox(tx, { + organisationId: updated.organisationId, + eventType: "agent.run.settled", + aggregateType: "agent_run", + aggregateId: updated.id, + queueName: "muster-notifications", + payload: { runId: updated.id, status: "failed" }, + idempotencyKey: `agent.run.settled:${updated.id}`, + traceId: redactObservationText( + this.request(updated).traceId ?? `agent-run-${updated.id}`, + ), + }); const [hunt] = await tx .update(schema.huntRuns) .set({ diff --git a/apps/web/app/api/v1/agent-harness/invocations/route.ts b/apps/web/app/api/v1/agent-harness/invocations/route.ts new file mode 100644 index 0000000..edf048c --- /dev/null +++ b/apps/web/app/api/v1/agent-harness/invocations/route.ts @@ -0,0 +1,20 @@ +import { GovernedAgentHarness } from "@muster/agent-harness"; +import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; + +export async function POST(request: Request) { + const traceId = requestTraceId(request); + try { + const subject = await apiSubject(request); + const idempotencyKey = request.headers.get("idempotency-key")?.trim(); + if (!idempotencyKey) + throw new Error("Idempotency-Key header is required for harness invocations"); + const data = await new GovernedAgentHarness().invoke( + subject, + await request.json(), + idempotencyKey, + ); + return Response.json({ data, traceId }, { status: data.duplicate ? 200 : 202 }); + } catch (error) { + return problemResponse(error, traceId); + } +} diff --git a/apps/web/app/api/v1/agent-harness/manifests/route.ts b/apps/web/app/api/v1/agent-harness/manifests/route.ts new file mode 100644 index 0000000..77f0723 --- /dev/null +++ b/apps/web/app/api/v1/agent-harness/manifests/route.ts @@ -0,0 +1,16 @@ +import { GovernedAgentHarness } from "@muster/agent-harness"; +import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; + +export async function GET(request: Request) { + const traceId = requestTraceId(request); + try { + const subject = await apiSubject(request); + return Response.json({ + protocolVersion: "muster.agent-harness/v1", + data: await new GovernedAgentHarness().manifest(subject), + traceId, + }); + } catch (error) { + return problemResponse(error, traceId); + } +} diff --git a/apps/web/app/api/v1/agent-harness/runs/[id]/route.ts b/apps/web/app/api/v1/agent-harness/runs/[id]/route.ts new file mode 100644 index 0000000..b8146af --- /dev/null +++ b/apps/web/app/api/v1/agent-harness/runs/[id]/route.ts @@ -0,0 +1,37 @@ +import { requireCapability } from "@muster/authz"; +import { GovernedAgentHarness } from "@muster/agent-harness"; +import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; + +type Context = { params: Promise<{ id: string }> }; + +export async function GET(request: Request, { params }: Context) { + const traceId = requestTraceId(request); + try { + const subject = await apiSubject(request); + const { id } = await params; + return Response.json({ + data: await new GovernedAgentHarness().read(subject, id), + traceId, + }); + } catch (error) { + return problemResponse(error, traceId); + } +} + +export async function DELETE(request: Request, { params }: Context) { + const traceId = requestTraceId(request); + try { + const subject = await apiSubject(request); + requireCapability(subject, "agents.cancel"); + const { id } = await params; + await new GovernedAgentHarness().read(subject, id); + const gateway = await fetch( + `${process.env.AGENT_GATEWAY_URL ?? "http://agent-gateway:3002"}/v1/runs/${encodeURIComponent(id)}/cancel`, + { method: "POST", signal: AbortSignal.timeout(5_000) }, + ); + if (!gateway.ok) throw new Error("Agent runtime did not accept cancellation"); + return Response.json({ data: await gateway.json(), traceId }, { status: 202 }); + } catch (error) { + return problemResponse(error, traceId); + } +} diff --git a/apps/web/app/api/v1/slack/commands/route.ts b/apps/web/app/api/v1/slack/commands/route.ts new file mode 100644 index 0000000..2222b9c --- /dev/null +++ b/apps/web/app/api/v1/slack/commands/route.ts @@ -0,0 +1,40 @@ +import { SlackGovernanceAdapter, verifySlackRequest } from "@muster/agent-harness"; + +export async function POST(request: Request) { + const rawBody = await request.text(); + const signingSecret = process.env.SLACK_SIGNING_SECRET; + if ( + !signingSecret || + !verifySlackRequest( + rawBody, + request.headers.get("x-slack-request-timestamp"), + request.headers.get("x-slack-signature"), + signingSecret, + ) + ) + return Response.json({ error: "invalid Slack signature" }, { status: 401 }); + const values = new URLSearchParams(rawBody); + const teamId = values.get("team_id"); + const userId = values.get("user_id"); + const channelId = values.get("channel_id"); + if (!teamId || !userId || !channelId) + return Response.json({ error: "invalid Slack command" }, { status: 400 }); + try { + await new SlackGovernanceAdapter().recordEvent(rawBody, { + type: "slash_command", + team_id: teamId, + event_id: values.get("trigger_id") ?? undefined, + event: { + type: "slash_command", + user: userId, + channel: channelId, + channel_type: values.get("channel_name") === "directmessage" ? "im" : "channel", + text: values.get("text") ?? "", + }, + }); + } catch { + // The signed request is acknowledged within Slack's deadline; outbox retry + // and installation health hold the durable failure path. + } + return Response.json({ response_type: "ephemeral", text: "Muster accepted your request." }); +} diff --git a/apps/web/app/api/v1/slack/events/route.ts b/apps/web/app/api/v1/slack/events/route.ts new file mode 100644 index 0000000..48e3f18 --- /dev/null +++ b/apps/web/app/api/v1/slack/events/route.ts @@ -0,0 +1,31 @@ +import { SlackGovernanceAdapter, verifySlackRequest } from "@muster/agent-harness"; + +export async function POST(request: Request) { + const rawBody = await request.text(); + const signingSecret = process.env.SLACK_SIGNING_SECRET; + if ( + !signingSecret || + !verifySlackRequest( + rawBody, + request.headers.get("x-slack-request-timestamp"), + request.headers.get("x-slack-signature"), + signingSecret, + ) + ) + return Response.json({ error: "invalid Slack signature" }, { status: 401 }); + let payload: Record; + try { + payload = JSON.parse(rawBody) as Record; + } catch { + return Response.json({ error: "invalid Slack payload" }, { status: 400 }); + } + if (payload.type === "url_verification" && typeof payload.challenge === "string") + return Response.json({ challenge: payload.challenge }); + try { + await new SlackGovernanceAdapter().recordEvent(rawBody, payload); + } catch { + // Acknowledge retried or currently unmapped events. Installation health + // records retain the authoritative failure path without leaking it to Slack. + } + return Response.json({ ok: true }); +} diff --git a/apps/web/app/api/v1/slack/exposures/route.ts b/apps/web/app/api/v1/slack/exposures/route.ts new file mode 100644 index 0000000..3d3a4ec --- /dev/null +++ b/apps/web/app/api/v1/slack/exposures/route.ts @@ -0,0 +1,39 @@ +import { SlackGovernanceAdapter } from "@muster/agent-harness"; +import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; +import { z } from "zod"; + +const ExposureSchema = z.object({ + installationId: z.string().uuid(), + agentId: z.string().uuid(), + enabled: z.boolean(), + isDefault: z.boolean(), + allowedChannelIds: z.array(z.string().trim().min(1).max(128)).max(500).optional(), + allowDirectMessages: z.boolean().optional(), + allowThreadContext: z.boolean().optional(), +}); + +export async function PUT(request: Request) { + const traceId = requestTraceId(request); + try { + const subject = await apiSubject(request); + const input = ExposureSchema.parse(await request.json()); + await new SlackGovernanceAdapter().configureExposure(subject, { + installationId: input.installationId, + agentId: input.agentId, + enabled: input.enabled, + isDefault: input.isDefault, + ...(input.allowedChannelIds === undefined + ? {} + : { allowedChannelIds: input.allowedChannelIds }), + ...(input.allowDirectMessages === undefined + ? {} + : { allowDirectMessages: input.allowDirectMessages }), + ...(input.allowThreadContext === undefined + ? {} + : { allowThreadContext: input.allowThreadContext }), + }); + return Response.json({ data: { status: "configured" }, traceId }); + } catch (error) { + return problemResponse(error, traceId); + } +} diff --git a/apps/web/app/api/v1/slack/health/route.ts b/apps/web/app/api/v1/slack/health/route.ts new file mode 100644 index 0000000..683b2ff --- /dev/null +++ b/apps/web/app/api/v1/slack/health/route.ts @@ -0,0 +1,12 @@ +import { SlackGovernanceAdapter } from "@muster/agent-harness"; +import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; + +export async function GET(request: Request) { + const traceId = requestTraceId(request); + try { + const subject = await apiSubject(request); + return Response.json({ data: await new SlackGovernanceAdapter().health(subject), traceId }); + } catch (error) { + return problemResponse(error, traceId); + } +} diff --git a/apps/web/app/api/v1/slack/identities/route.ts b/apps/web/app/api/v1/slack/identities/route.ts new file mode 100644 index 0000000..0001667 --- /dev/null +++ b/apps/web/app/api/v1/slack/identities/route.ts @@ -0,0 +1,23 @@ +import { SlackGovernanceAdapter } from "@muster/agent-harness"; +import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; +import { z } from "zod"; + +const MappingSchema = z.object({ + installationId: z.string().uuid(), + slackUserId: z.string().trim().min(1).max(128), + actorId: z.string().uuid(), +}); + +export async function POST(request: Request) { + const traceId = requestTraceId(request); + try { + const subject = await apiSubject(request); + await new SlackGovernanceAdapter().mapIdentity( + subject, + MappingSchema.parse(await request.json()), + ); + return Response.json({ data: { status: "active" }, traceId }, { status: 201 }); + } catch (error) { + return problemResponse(error, traceId); + } +} diff --git a/apps/web/app/api/v1/slack/install/route.ts b/apps/web/app/api/v1/slack/install/route.ts new file mode 100644 index 0000000..7984866 --- /dev/null +++ b/apps/web/app/api/v1/slack/install/route.ts @@ -0,0 +1,29 @@ +import { requireCapability } from "@muster/authz"; +import { signSlackOAuthState } from "@muster/agent-harness"; +import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; + +const scopes = ["app_mentions:read", "chat:write", "commands", "im:history"]; + +export async function GET(request: Request) { + const traceId = requestTraceId(request); + try { + const subject = await apiSubject(request); + requireCapability(subject, "administration.manage"); + const clientId = process.env.SLACK_CLIENT_ID; + const redirectUri = process.env.SLACK_REDIRECT_URI; + if (!clientId || !redirectUri) throw new Error("Slack OAuth is not configured"); + const state = signSlackOAuthState({ + organisationId: subject.organisationId, + actorId: subject.actorId, + expiresAt: Date.now() + 10 * 60_000, + }); + const authorizationUrl = new URL("https://slack.com/oauth/v2/authorize"); + authorizationUrl.searchParams.set("client_id", clientId); + authorizationUrl.searchParams.set("redirect_uri", redirectUri); + authorizationUrl.searchParams.set("scope", scopes.join(",")); + authorizationUrl.searchParams.set("state", state); + return Response.json({ data: { authorizationUrl: authorizationUrl.toString() }, traceId }); + } catch (error) { + return problemResponse(error, traceId); + } +} diff --git a/apps/web/app/api/v1/slack/interactions/route.ts b/apps/web/app/api/v1/slack/interactions/route.ts new file mode 100644 index 0000000..7b76128 --- /dev/null +++ b/apps/web/app/api/v1/slack/interactions/route.ts @@ -0,0 +1,26 @@ +import { SlackGovernanceAdapter, verifySlackRequest } from "@muster/agent-harness"; + +export async function POST(request: Request) { + const rawBody = await request.text(); + const signingSecret = process.env.SLACK_SIGNING_SECRET; + if ( + !signingSecret || + !verifySlackRequest( + rawBody, + request.headers.get("x-slack-request-timestamp"), + request.headers.get("x-slack-signature"), + signingSecret, + ) + ) + return Response.json({ error: "invalid Slack signature" }, { status: 401 }); + const encoded = new URLSearchParams(rawBody).get("payload"); + if (!encoded) return Response.json({ error: "invalid Slack interaction" }, { status: 400 }); + try { + const payload = JSON.parse(encoded) as Record; + await new SlackGovernanceAdapter().recordEvent(rawBody, payload); + } catch { + // Slack requires a fast acknowledgement. The durable inbox carries retries + // and operator-visible delivery health without exposing internals to Slack. + } + return Response.json({ ok: true }); +} diff --git a/apps/web/app/api/v1/slack/oauth/callback/route.ts b/apps/web/app/api/v1/slack/oauth/callback/route.ts new file mode 100644 index 0000000..6c53077 --- /dev/null +++ b/apps/web/app/api/v1/slack/oauth/callback/route.ts @@ -0,0 +1,59 @@ +import { + SlackGovernanceAdapter, + verifySlackOAuthState, +} from "@muster/agent-harness"; +import { capabilities, type AuthorisationSubject } from "@muster/authz"; +import { database, schema } from "@muster/database"; +import { and, eq } from "drizzle-orm"; +import { problemResponse, requestTraceId } from "@/lib/api-context"; + +export async function GET(request: Request) { + const traceId = requestTraceId(request); + try { + const url = new URL(request.url); + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + if (!code || !state) throw new Error("Slack OAuth callback is incomplete"); + const verified = verifySlackOAuthState(state); + const [actor] = await database() + .select({ capabilities: schema.actors.capabilityAssignments }) + .from(schema.actors) + .where( + and( + eq(schema.actors.id, verified.actorId), + eq(schema.actors.organisationId, verified.organisationId), + eq(schema.actors.actorType, "human"), + eq(schema.actors.status, "active"), + ), + ) + .limit(1); + if (!actor || !Array.isArray(actor.capabilities)) + throw new Error("Slack OAuth installer is no longer authorised"); + const subject: AuthorisationSubject = { + actorId: verified.actorId, + organisationId: verified.organisationId, + capabilities: new Set( + actor.capabilities.filter( + (capability): capability is (typeof capabilities)[number] => + typeof capability === "string" && + capabilities.includes(capability as (typeof capabilities)[number]), + ), + ), + }; + const installation = await new SlackGovernanceAdapter().install( + subject, + code, + process.env.SLACK_REDIRECT_URI ?? "", + ); + return Response.json({ + data: { + id: installation.id, + teamId: installation.teamId, + status: installation.status, + }, + traceId, + }); + } catch (error) { + return problemResponse(error, traceId); + } +} diff --git a/apps/web/package.json b/apps/web/package.json index 0171c41..64b86ce 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "@fontsource-variable/ibm-plex-sans": "^5.2.7", "@fontsource-variable/jetbrains-mono": "^5.2.7", "@muster/agents": "workspace:*", + "@muster/agent-harness": "workspace:*", "@muster/auth": "workspace:*", "@muster/authz": "workspace:*", "@muster/config": "workspace:*", diff --git a/apps/worker/package.json b/apps/worker/package.json index 38b40aa..37a7fa9 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -14,6 +14,7 @@ "dependencies": { "@muster/agents": "workspace:*", "@muster/config": "workspace:*", + "@muster/agent-harness": "workspace:*", "@muster/contracts": "workspace:*", "@muster/database": "workspace:*", "@muster/evidence": "workspace:*", diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 1656010..7ab1490 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -2,6 +2,7 @@ import { createServer } from "node:http"; import { createHash } from "node:crypto"; import nodemailer from "nodemailer"; import { Queue, Worker, type JobsOptions, type Processor } from "bullmq"; +import { deliverSlackRun, processSlackInboxEvent } from "@muster/agent-harness"; import { queueNames, ReportManifestSchema, @@ -134,6 +135,18 @@ const authoritativeProcessor: Processor = async (job) => { finalResearchAttempt(job.attemptsMade, job.opts.attempts ?? 1), ); } + if ( + job.queueName === "muster-notifications" && + job.name === "slack.event.received" + ) { + await processSlackInboxEvent(job.data.aggregateId); + } + if ( + job.queueName === "muster-notifications" && + job.name === "agent.run.settled" + ) { + await deliverSlackRun(job.data.aggregateId); + } if ( job.queueName === "muster-integrations" && job.name === "integration.action.queued" diff --git a/docs/integrations/agent-harness.md b/docs/integrations/agent-harness.md new file mode 100644 index 0000000..9e1d99b --- /dev/null +++ b/docs/integrations/agent-harness.md @@ -0,0 +1,59 @@ +# Governed agent harnesses + +Muster agents are exposed through `muster.agent-harness/v1`. Every adapter uses +the same organisation-scoped manifest, capability checks, approval model, durable +`agent_runs` record, outbox event, audit event, cancellation, and typed result. +Redis and BullMQ only execute the durable PostgreSQL state transition. + +## Portable adapters + +Fetch `GET /api/v1/agent-harness/manifests` to discover only agents the current +actor may invoke. Submit `POST /api/v1/agent-harness/invocations` with an +`Idempotency-Key` header and a body such as: + +```json +{ + "agentKey": "Jessie", + "mode": "http", + "input": { "prompt": "Summarise this bounded investigation." } +} +``` + +Poll `GET /api/v1/agent-harness/runs/:id`; `DELETE` on that resource requests a +capability-checked cancellation. Hermes, MCP, and CLI adapters use the same +manifest and invocation shape, changing only `mode` to `hermes`, `mcp`, or `cli`. +Adapters must never pass prompts, connector tokens, or restricted evidence to a +different tenant. + +## Slack + +An organisation administrator starts OAuth at `GET /api/v1/slack/install`. +Muster requires `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, `SLACK_REDIRECT_URI`, +`SLACK_SIGNING_SECRET`, `SLACK_OAUTH_STATE_SECRET`, and +`CONNECTOR_ENCRYPTION_KEY`. Bot tokens and inbound payloads are encrypted before +they reach PostgreSQL; only hashes, bounded error text, and audit metadata are +available for operations. + +Configure Slack as follows: + +- Redirect URL: `/api/v1/slack/oauth/callback` +- Events URL: `/api/v1/slack/events` +- Interactivity URL: `/api/v1/slack/interactions` +- Slash command URL: `/api/v1/slack/commands` +- Bot scopes: `app_mentions:read`, `chat:write`, `commands`, and `im:history` +- Subscribe to app mentions and direct messages; add the Slack Assistant event + subscriptions when Assistant Threads are enabled for the workspace. + +Administrators map each Slack user to an active Muster human actor with +`POST /api/v1/slack/identities`, then expose approved agents and channel policy +using `PUT /api/v1/slack/exposures`. `GET /api/v1/slack/health` is the admin +health view for installation status, scopes, delivery time, and redacted errors. +Slack accepts only fresh HMAC-signed requests, persists them using a workspace +event idempotency key, acknowledges immediately, and invokes agents asynchronously +from the outbox. Result messages are updated in their original thread and offer +capability-checked Cancel, Retry, and View in Muster actions. + +For Socket Mode, acknowledge Slack's `envelope_id` immediately and pass the +envelope payload to `SlackGovernanceAdapter.recordSocketEnvelope`. It shares the +same encrypted inbox and idempotency path as Events API delivery, so reconnects +cannot duplicate a run. diff --git a/packages/agent-harness/package.json b/packages/agent-harness/package.json new file mode 100644 index 0000000..e085e0c --- /dev/null +++ b/packages/agent-harness/package.json @@ -0,0 +1,33 @@ +{ + "name": "@muster/agent-harness", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "development": "./src/index.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "tsc -p tsconfig.json --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@muster/authz": "workspace:*", + "@muster/config": "workspace:*", + "@muster/contracts": "workspace:*", + "@muster/database": "workspace:*", + "@muster/integrations": "workspace:*", + "drizzle-orm": "0.45.2", + "zod": "4.4.3" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "typescript": "catalog:", + "vitest": "4.1.10" + } +} diff --git a/packages/agent-harness/src/index.test.ts b/packages/agent-harness/src/index.test.ts new file mode 100644 index 0000000..301f32a --- /dev/null +++ b/packages/agent-harness/src/index.test.ts @@ -0,0 +1,38 @@ +import { createHmac } from "node:crypto"; +import { describe, expect, it } from "vitest"; +import { + signSlackOAuthState, + verifySlackOAuthState, + verifySlackRequest, +} from "./index"; + +describe("Slack governed harness boundary", () => { + it("accepts only fresh, correctly signed Slack requests", () => { + const now = 1_700_000_000_000; + const timestamp = String(now / 1_000); + const raw = '{"type":"event_callback"}'; + const signature = `v0=${createHmac("sha256", "test-secret") + .update(`v0:${timestamp}:${raw}`) + .digest("hex")}`; + + expect(verifySlackRequest(raw, timestamp, signature, "test-secret", now)).toBe(true); + expect(verifySlackRequest(raw, timestamp, signature, "wrong-secret", now)).toBe(false); + expect(verifySlackRequest(raw, timestamp, signature, "test-secret", now + 300_001)).toBe(false); + }); + + it("binds OAuth state to a signed, unexpired actor and organisation", () => { + const original = process.env.SLACK_OAUTH_STATE_SECRET; + process.env.SLACK_OAUTH_STATE_SECRET = "state-secret"; + const state = signSlackOAuthState({ + organisationId: "00000000-0000-4000-8000-000000000001", + actorId: "00000000-0000-4000-8000-000000000002", + expiresAt: Date.now() + 60_000, + }); + expect(verifySlackOAuthState(state).actorId).toBe( + "00000000-0000-4000-8000-000000000002", + ); + expect(() => verifySlackOAuthState(`${state}x`)).toThrow("Invalid Slack OAuth state"); + if (original === undefined) delete process.env.SLACK_OAUTH_STATE_SECRET; + else process.env.SLACK_OAUTH_STATE_SECRET = original; + }); +}); diff --git a/packages/agent-harness/src/index.ts b/packages/agent-harness/src/index.ts new file mode 100644 index 0000000..2a8e42b --- /dev/null +++ b/packages/agent-harness/src/index.ts @@ -0,0 +1,1106 @@ +import { createHash, createHmac, timingSafeEqual } from "node:crypto"; +import { redactObservationText } from "@muster/config"; +import { + AgentHarnessInvokeSchema, + AgentHarnessManifestSchema, + AgentHarnessRunSchema, + type AgentHarnessInvoke, + type AgentHarnessInvocationMode, + type AgentHarnessManifest, + type AgentHarnessRun, +} from "@muster/contracts"; +import { + type AuthorisationSubject, + capabilities, + requireCapability, + type Capability, +} from "@muster/authz"; +import { + appendAuditEvent, + database, + newId, + schema, + writeOutbox, +} from "@muster/database"; +import { + decryptConnectorPayload, + encryptConnectorPayload, +} from "@muster/integrations"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod"; + +const protocolVersion = "muster.agent-harness/v1" as const; +const supportedModes: AgentHarnessInvocationMode[] = [ + "slack", + "hermes", + "mcp", + "cli", + "http", +]; + +function asCapabilities(value: unknown): Capability[] { + if (!Array.isArray(value)) return []; + return value.filter( + (item): item is Capability => + typeof item === "string" && capabilities.includes(item as Capability), + ); +} + +function agentRequirements(value: unknown): Capability[] { + return asCapabilities(value); +} + +function encryptionKey() { + const key = process.env.CONNECTOR_ENCRYPTION_KEY; + if (!key) throw new Error("Connector encryption is not configured"); + return key; +} + +function traceId(value: string | undefined) { + return redactObservationText(value ?? crypto.randomUUID(), { + maxStringLength: 160, + }); +} + +export class GovernedAgentHarness { + constructor(private readonly db = database()) {} + + async manifest(subject: AuthorisationSubject): Promise { + requireCapability(subject, "agents.read"); + const definitions = await this.db + .select() + .from(schema.agentDefinitions) + .where( + and( + eq(schema.agentDefinitions.organisationId, subject.organisationId), + eq(schema.agentDefinitions.status, "active"), + eq(schema.agentDefinitions.killSwitch, false), + ), + ); + return definitions + .filter((definition) => + agentRequirements(definition.capabilityRequirements).every((capability) => + subject.capabilities.has(capability), + ), + ) + .map((definition) => + AgentHarnessManifestSchema.parse({ + protocolVersion, + key: definition.name, + version: definition.systemPromptVersion, + name: definition.name, + description: definition.description, + invocationModes: supportedModes, + inputSchema: "muster.agent-harness.input/v1", + outputSchema: "muster.agent.structured/v1", + requiredCapabilities: agentRequirements( + definition.capabilityRequirements, + ), + approvalBehavior: + definition.requestedPermissionMode === "approval_gated" + ? "governed_actions" + : "none", + lifecycle: "active", + }), + ); + } + + async invoke( + subject: AuthorisationSubject, + rawInput: AgentHarnessInvoke, + idempotencyKey: string, + ): Promise { + requireCapability(subject, "agents.invoke"); + const input = AgentHarnessInvokeSchema.parse(rawInput); + const correlationId = traceId(input.correlationId); + const [definition] = await this.db + .select() + .from(schema.agentDefinitions) + .where( + and( + eq(schema.agentDefinitions.organisationId, subject.organisationId), + eq(schema.agentDefinitions.name, input.agentKey), + eq(schema.agentDefinitions.status, "active"), + eq(schema.agentDefinitions.killSwitch, false), + ), + ) + .limit(1); + if (!definition) throw new Error("Active agent is not exposed"); + for (const capability of agentRequirements( + definition.capabilityRequirements, + )) + requireCapability(subject, capability); + if (input.input.roomId) { + const [membership] = await this.db + .select({ roomId: schema.roomMemberships.roomId }) + .from(schema.roomMemberships) + .innerJoin( + schema.rooms, + and( + eq(schema.rooms.id, schema.roomMemberships.roomId), + eq(schema.rooms.organisationId, subject.organisationId), + ), + ) + .where( + and( + eq(schema.roomMemberships.roomId, input.input.roomId), + eq(schema.roomMemberships.actorId, subject.actorId), + ), + ) + .limit(1); + if (!membership) throw new Error("Room membership required"); + } + if (input.input.investigationId) { + const [investigation] = await this.db + .select({ id: schema.investigations.id }) + .from(schema.investigations) + .where( + and( + eq(schema.investigations.id, input.input.investigationId), + eq(schema.investigations.organisationId, subject.organisationId), + ), + ) + .limit(1); + if (!investigation) throw new Error("Investigation not found"); + } + const prompt = redactObservationText(input.input.prompt, { + maxStringLength: 4_000, + }); + const inputHash = createHash("sha256").update(prompt).digest("hex"); + const deadlineAt = new Date( + Date.now() + definition.maximumRuntimeSeconds * 1_000, + ); + const accepted = await this.db.transaction(async (tx) => { + const [inserted] = await tx + .insert(schema.agentRuns) + .values({ + id: newId(), + agentId: definition.id, + organisationId: subject.organisationId, + roomId: input.input.roomId, + investigationId: input.input.investigationId, + requestedByActorId: subject.actorId, + trigger: `harness:${input.mode}`, + status: "queued", + request: { + humanRequest: prompt, + traceId: correlationId, + harness: { + protocolVersion, + mode: input.mode, + taskId: input.input.taskId, + caseId: input.input.caseId, + }, + }, + progress: { stage: "queued", percent: 0 }, + deadlineAt, + inputHash, + promptVersion: definition.systemPromptVersion, + runtime: definition.runtime, + model: definition.model, + maximumRuntimeSeconds: definition.maximumRuntimeSeconds, + maximumTokenBudget: definition.maximumTokenBudget, + maximumCostCents: definition.maximumCostCents, + idempotencyKey, + }) + .onConflictDoNothing() + .returning(); + const run = + inserted ?? + ( + await tx + .select() + .from(schema.agentRuns) + .where( + and( + eq(schema.agentRuns.organisationId, subject.organisationId), + eq(schema.agentRuns.idempotencyKey, idempotencyKey), + ), + ) + .limit(1) + )[0]; + if (!run) throw new Error("Could not accept harness invocation"); + if (inserted) { + await tx.insert(schema.agentRunEvents).values({ + id: newId(), + organisationId: subject.organisationId, + runId: run.id, + eventType: "queued", + message: "Portable governed harness invocation accepted", + payload: { mode: input.mode, correlationId }, + }); + await writeOutbox(tx, { + organisationId: subject.organisationId, + eventType: "agent.run.queued", + aggregateType: "agent_run", + aggregateId: run.id, + queueName: "muster-agents", + payload: { runId: run.id }, + idempotencyKey: `agent.run.queued:${run.id}`, + traceId: correlationId, + }); + await appendAuditEvent(tx, { + organisationId: subject.organisationId, + actorId: subject.actorId, + actorType: "human", + action: "agent.harness.invoked", + targetType: "agent_run", + targetId: run.id, + metadata: { + agentKey: definition.name, + mode: input.mode, + inputHash, + idempotencyKey, + }, + traceId: correlationId, + }); + } + return { run, duplicate: !inserted }; + }); + return AgentHarnessRunSchema.parse({ + protocolVersion, + runId: accepted.run.id, + status: accepted.run.status, + agentKey: definition.name, + correlationId, + duplicate: accepted.duplicate, + result: accepted.run.structuredOutput ?? null, + }); + } + + async read(subject: AuthorisationSubject, runId: string): Promise { + requireCapability(subject, "agents.read"); + const [row] = await this.db + .select({ run: schema.agentRuns, agentKey: schema.agentDefinitions.name }) + .from(schema.agentRuns) + .innerJoin( + schema.agentDefinitions, + and( + eq(schema.agentDefinitions.id, schema.agentRuns.agentId), + eq( + schema.agentDefinitions.organisationId, + schema.agentRuns.organisationId, + ), + ), + ) + .where( + and( + eq(schema.agentRuns.id, runId), + eq(schema.agentRuns.organisationId, subject.organisationId), + ), + ) + .limit(1); + if (!row) throw new Error("Agent run not found"); + const request = row.run.request as { traceId?: unknown }; + return AgentHarnessRunSchema.parse({ + protocolVersion, + runId: row.run.id, + status: row.run.status, + agentKey: row.agentKey, + correlationId: + typeof request.traceId === "string" ? request.traceId : row.run.id, + duplicate: false, + result: row.run.structuredOutput ?? null, + }); + } +} + +const SlackOAuthResponseSchema = z.object({ + ok: z.literal(true), + access_token: z.string().min(1), + app_id: z.string().optional(), + bot_user_id: z.string().optional(), + team: z.object({ id: z.string().min(1), name: z.string().optional() }), + enterprise: z.object({ id: z.string().optional() }).optional(), + scope: z.string().optional(), +}); + +export function verifySlackRequest( + rawBody: string, + timestamp: string | null, + signature: string | null, + signingSecret: string, + now = Date.now(), +) { + if (!timestamp || !signature || !/^v0=[a-f0-9]{64}$/i.test(signature)) + return false; + const age = Math.abs(now - Number(timestamp) * 1_000); + if (!Number.isFinite(age) || age > 300_000) return false; + const expected = `v0=${createHmac("sha256", signingSecret) + .update(`v0:${timestamp}:${rawBody}`) + .digest("hex")}`; + return timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); +} + +export function signSlackOAuthState(input: { + organisationId: string; + actorId: string; + expiresAt: number; +}) { + const secret = process.env.SLACK_OAUTH_STATE_SECRET ?? process.env.BETTER_AUTH_SECRET; + if (!secret) throw new Error("Slack OAuth state secret is not configured"); + const payload = Buffer.from(JSON.stringify(input)).toString("base64url"); + const signature = createHmac("sha256", secret).update(payload).digest("base64url"); + return `${payload}.${signature}`; +} + +export function verifySlackOAuthState(value: string) { + const [payload, signature] = value.split("."); + const secret = process.env.SLACK_OAUTH_STATE_SECRET ?? process.env.BETTER_AUTH_SECRET; + if (!payload || !signature || !secret) throw new Error("Invalid Slack OAuth state"); + const expected = createHmac("sha256", secret).update(payload).digest("base64url"); + if ( + expected.length !== signature.length || + !timingSafeEqual(Buffer.from(expected), Buffer.from(signature)) + ) + throw new Error("Invalid Slack OAuth state"); + const input = z + .object({ + organisationId: z.string().uuid(), + actorId: z.string().uuid(), + expiresAt: z.number(), + }) + .parse(JSON.parse(Buffer.from(payload, "base64url").toString("utf8"))); + if (input.expiresAt < Date.now()) throw new Error("Expired Slack OAuth state"); + return input; +} + +export class SlackGovernanceAdapter { + constructor(private readonly db = database()) {} + + async install(subject: AuthorisationSubject, code: string, redirectUri: string) { + requireCapability(subject, "administration.manage"); + const clientId = process.env.SLACK_CLIENT_ID; + const clientSecret = process.env.SLACK_CLIENT_SECRET; + if (!clientId || !clientSecret) throw new Error("Slack OAuth is not configured"); + const response = await fetch("https://slack.com/api/oauth.v2.access", { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + code, + client_id: clientId, + client_secret: clientSecret, + redirect_uri: redirectUri, + }), + signal: AbortSignal.timeout(10_000), + }); + const payload = SlackOAuthResponseSchema.parse(await response.json()); + const scopes = payload.scope?.split(",").filter(Boolean) ?? []; + const encryptedBotToken = encryptConnectorPayload( + { token: payload.access_token }, + encryptionKey(), + ); + return this.db.transaction(async (tx) => { + const [installation] = await tx + .insert(schema.slackInstallations) + .values({ + id: newId(), + organisationId: subject.organisationId, + teamId: payload.team.id, + teamName: payload.team.name, + enterpriseId: payload.enterprise?.id, + botUserId: payload.bot_user_id, + scopes, + encryptedBotToken, + installedByActorId: subject.actorId, + status: "active", + }) + .onConflictDoUpdate({ + target: schema.slackInstallations.teamId, + set: { + organisationId: subject.organisationId, + teamName: payload.team.name, + enterpriseId: payload.enterprise?.id, + botUserId: payload.bot_user_id, + scopes, + encryptedBotToken, + installedByActorId: subject.actorId, + status: "active", + revokedAt: null, + updatedAt: new Date(), + }, + }) + .returning(); + if (!installation) throw new Error("Slack installation was not persisted"); + await appendAuditEvent(tx, { + organisationId: subject.organisationId, + actorId: subject.actorId, + actorType: "human", + action: "slack.installation.connected", + targetType: "slack_installation", + targetId: installation.id, + metadata: { teamId: payload.team.id, scopes }, + traceId: crypto.randomUUID(), + }); + return installation; + }); + } + + async recordEvent(rawBody: string, payload: Record) { + const teamId = + typeof payload.team_id === "string" + ? payload.team_id + : typeof payload.team === "object" && payload.team && "id" in payload.team + ? String((payload.team as { id: unknown }).id) + : undefined; + if (!teamId) throw new Error("Slack workspace is missing"); + const [installation] = await this.db + .select() + .from(schema.slackInstallations) + .where( + and( + eq(schema.slackInstallations.teamId, teamId), + eq(schema.slackInstallations.status, "active"), + ), + ) + .limit(1); + if (!installation) throw new Error("Slack workspace is not connected"); + const eventId = + typeof payload.event_id === "string" + ? payload.event_id + : createHash("sha256").update(rawBody).digest("hex"); + const eventType = + typeof payload.type === "string" ? payload.type : "event_callback"; + const payloadHash = createHash("sha256").update(rawBody).digest("hex"); + return this.db.transaction(async (tx) => { + const [inserted] = await tx + .insert(schema.slackInboxEvents) + .values({ + id: newId(), + organisationId: installation.organisationId, + installationId: installation.id, + eventId, + eventType, + payloadHash, + encryptedPayload: encryptConnectorPayload(payload, encryptionKey()), + }) + .onConflictDoNothing() + .returning(); + if (inserted) { + await writeOutbox(tx, { + organisationId: installation.organisationId, + eventType: "slack.event.received", + aggregateType: "slack_inbox_event", + aggregateId: inserted.id, + queueName: "muster-notifications", + payload: { inboxEventId: inserted.id }, + idempotencyKey: `slack.event:${installation.id}:${eventId}`, + traceId: eventId, + }); + } + return { installation, inboxEvent: inserted, duplicate: !inserted }; + }); + } + + /** + * Socket Mode adapters call this after acknowledging Slack's envelope. The + * same encrypted inbox and idempotency path is deliberately shared with HTTP + * Events API delivery so a reconnect cannot invoke an agent twice. + */ + async recordSocketEnvelope(envelope: { + envelope_id: string; + payload: Record; + }) { + if (!envelope.envelope_id.trim()) throw new Error("Slack envelope is missing"); + return this.recordEvent( + JSON.stringify({ envelope_id: envelope.envelope_id, payload: envelope.payload }), + envelope.payload, + ); + } + + async health(subject: AuthorisationSubject) { + requireCapability(subject, "administration.manage"); + return this.db + .select({ + id: schema.slackInstallations.id, + teamId: schema.slackInstallations.teamId, + teamName: schema.slackInstallations.teamName, + scopes: schema.slackInstallations.scopes, + status: schema.slackInstallations.status, + lastHealthAt: schema.slackInstallations.lastHealthAt, + lastDeliveryAt: schema.slackInstallations.lastDeliveryAt, + lastError: schema.slackInstallations.lastError, + }) + .from(schema.slackInstallations) + .where(eq(schema.slackInstallations.organisationId, subject.organisationId)); + } + + async mapIdentity( + subject: AuthorisationSubject, + input: { installationId: string; slackUserId: string; actorId: string }, + ) { + requireCapability(subject, "administration.manage"); + const [installationRows, actorRows] = await Promise.all([ + this.db + .select({ id: schema.slackInstallations.id }) + .from(schema.slackInstallations) + .where( + and( + eq(schema.slackInstallations.id, input.installationId), + eq(schema.slackInstallations.organisationId, subject.organisationId), + eq(schema.slackInstallations.status, "active"), + ), + ) + .limit(1), + this.db + .select({ id: schema.actors.id }) + .from(schema.actors) + .where( + and( + eq(schema.actors.id, input.actorId), + eq(schema.actors.organisationId, subject.organisationId), + eq(schema.actors.actorType, "human"), + eq(schema.actors.status, "active"), + ), + ) + .limit(1), + ]); + const installation = installationRows[0]; + const actor = actorRows[0]; + if (!installation || !actor) + throw new Error("Slack installation or actor not found"); + await this.db + .insert(schema.slackIdentityMappings) + .values({ + id: newId(), + organisationId: subject.organisationId, + installationId: installation.id, + slackUserId: input.slackUserId.trim(), + actorId: actor.id, + createdByActorId: subject.actorId, + }) + .onConflictDoUpdate({ + target: [ + schema.slackIdentityMappings.installationId, + schema.slackIdentityMappings.slackUserId, + ], + set: { actorId: actor.id, status: "active", revokedAt: null }, + }); + } + + async configureExposure( + subject: AuthorisationSubject, + input: { + installationId: string; + agentId: string; + enabled: boolean; + isDefault: boolean; + allowedChannelIds?: string[]; + allowDirectMessages?: boolean; + allowThreadContext?: boolean; + }, + ) { + requireCapability(subject, "administration.manage"); + const [installationRows, agentRows] = await Promise.all([ + this.db + .select({ id: schema.slackInstallations.id }) + .from(schema.slackInstallations) + .where( + and( + eq(schema.slackInstallations.id, input.installationId), + eq(schema.slackInstallations.organisationId, subject.organisationId), + ), + ) + .limit(1), + this.db + .select({ id: schema.agentDefinitions.id }) + .from(schema.agentDefinitions) + .where( + and( + eq(schema.agentDefinitions.id, input.agentId), + eq(schema.agentDefinitions.organisationId, subject.organisationId), + ), + ) + .limit(1), + ]); + const installation = installationRows[0]; + const agent = agentRows[0]; + if (!installation || !agent) + throw new Error("Slack installation or agent not found"); + await this.db.transaction(async (tx) => { + if (input.isDefault) + await tx + .update(schema.slackAgentExposures) + .set({ isDefault: false, updatedAt: new Date() }) + .where( + and( + eq(schema.slackAgentExposures.installationId, installation.id), + eq(schema.slackAgentExposures.organisationId, subject.organisationId), + ), + ); + await tx + .insert(schema.slackAgentExposures) + .values({ + id: newId(), + organisationId: subject.organisationId, + installationId: installation.id, + agentId: agent.id, + enabled: input.enabled, + isDefault: input.isDefault, + allowedChannelIds: input.allowedChannelIds ?? [], + allowDirectMessages: input.allowDirectMessages ?? true, + allowThreadContext: input.allowThreadContext ?? false, + updatedByActorId: subject.actorId, + }) + .onConflictDoUpdate({ + target: [ + schema.slackAgentExposures.installationId, + schema.slackAgentExposures.agentId, + ], + set: { + enabled: input.enabled, + isDefault: input.isDefault, + allowedChannelIds: input.allowedChannelIds ?? [], + allowDirectMessages: input.allowDirectMessages ?? true, + allowThreadContext: input.allowThreadContext ?? false, + updatedByActorId: subject.actorId, + updatedAt: new Date(), + }, + }); + await appendAuditEvent(tx, { + organisationId: subject.organisationId, + actorId: subject.actorId, + actorType: "human", + action: "slack.agent.exposure.updated", + targetType: "agent", + targetId: agent.id, + metadata: { + installationId: installation.id, + enabled: input.enabled, + isDefault: input.isDefault, + }, + traceId: crypto.randomUUID(), + }); + }); + } +} + +type SlackMessage = { + type?: string; + user?: string; + channel?: string; + channel_type?: string; + text?: string; + thread_ts?: string; + ts?: string; +}; + +type SlackEnvelope = { + event?: SlackMessage; + user?: { id?: string }; + channel?: { id?: string }; + message?: { thread_ts?: string; ts?: string }; + container?: { thread_ts?: string; message_ts?: string }; + type?: string; + actions?: Array<{ action_id?: string; value?: string }>; +}; + +function slackText(value: unknown, max = 2_000) { + return redactObservationText(typeof value === "string" ? value : "", { + maxStringLength: max, + }); +} + +async function slackApi(token: string, method: string, body: Record) { + const response = await fetch(`https://slack.com/api/${method}`, { + method: "POST", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json; charset=utf-8", + }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(10_000), + }); + const payload = (await response.json()) as { ok?: boolean; ts?: string; error?: string }; + if (!response.ok || !payload.ok) + throw new Error(`Slack ${method} failed: ${payload.error ?? response.status}`); + return payload; +} + +function resultBlocks(agentName: string, status: string, output: unknown) { + const result = + output && typeof output === "object" && !Array.isArray(output) + ? (output as Record) + : {}; + const summary = slackText(result.summary, 1_500) || "No typed result was produced."; + const confidence = + typeof result.confidence === "number" + ? `\n*Confidence:* ${Math.round(result.confidence * 100)}%` + : ""; + const gaps = Array.isArray(result.gaps) + ? slackText(result.gaps.filter((gap): gap is string => typeof gap === "string").slice(0, 3).join("; "), 700) + : ""; + const actions: Array<{ + type: "button"; + action_id: string; + text: { type: "plain_text"; text: string }; + value: string; + style?: "danger"; + }> = [ + { + type: "button", + action_id: "muster.view_in_muster", + text: { type: "plain_text", text: "View in Muster" }, + value: "view", + }, + ]; + if (["queued", "running", "waiting_sources", "awaiting_approval"].includes(status)) + actions.unshift({ + type: "button", + action_id: "muster.cancel", + text: { type: "plain_text", text: "Cancel" }, + style: "danger", + value: typeof result.runId === "string" ? result.runId : "", + }); + if (["failed", "cancelled"].includes(status) && typeof result.runId === "string") + actions.unshift({ + type: "button", + action_id: "muster.retry", + text: { type: "plain_text", text: "Retry" }, + value: result.runId, + }); + return [ + { + type: "section", + text: { type: "mrkdwn", text: `*${agentName}* — ${status}` }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `${summary}${confidence}${gaps ? `\n*Gaps:* ${gaps}` : ""}`, + }, + }, + { + type: "actions", + elements: actions, + }, + ]; +} + +export async function processSlackInboxEvent(inboxEventId: string) { + const db = database(); + const [row] = await db + .select({ + inbox: schema.slackInboxEvents, + installation: schema.slackInstallations, + }) + .from(schema.slackInboxEvents) + .innerJoin( + schema.slackInstallations, + and( + eq(schema.slackInstallations.id, schema.slackInboxEvents.installationId), + eq( + schema.slackInstallations.organisationId, + schema.slackInboxEvents.organisationId, + ), + ), + ) + .where(eq(schema.slackInboxEvents.id, inboxEventId)) + .limit(1); + if (!row || row.inbox.status === "processed") return; + const payload = decryptConnectorPayload( + row.inbox.encryptedPayload, + encryptionKey(), + ) as SlackEnvelope; + const event: SlackMessage = payload.event ?? {}; + const slackUserId = event.user ?? payload.user?.id; + const channelId = event.channel ?? payload.channel?.id; + const threadTs = + event.thread_ts ?? + payload.message?.thread_ts ?? + payload.container?.thread_ts ?? + event.ts ?? + payload.message?.ts ?? + payload.container?.message_ts; + if (!slackUserId || !channelId) { + await db + .update(schema.slackInboxEvents) + .set({ status: "ignored", processedAt: new Date() }) + .where(eq(schema.slackInboxEvents.id, row.inbox.id)); + return; + } + const [identity] = await db + .select({ actor: schema.actors }) + .from(schema.slackIdentityMappings) + .innerJoin( + schema.actors, + and( + eq(schema.actors.id, schema.slackIdentityMappings.actorId), + eq(schema.actors.organisationId, row.inbox.organisationId), + ), + ) + .where( + and( + eq(schema.slackIdentityMappings.installationId, row.installation.id), + eq(schema.slackIdentityMappings.slackUserId, slackUserId), + eq(schema.slackIdentityMappings.status, "active"), + eq(schema.actors.status, "active"), + ), + ) + .limit(1); + if (!identity) { + await db + .update(schema.slackInboxEvents) + .set({ status: "ignored", processedAt: new Date(), error: "identity_unmapped" }) + .where(eq(schema.slackInboxEvents.id, row.inbox.id)); + return; + } + const subject: AuthorisationSubject = { + actorId: identity.actor.id, + organisationId: row.inbox.organisationId, + capabilities: new Set(asCapabilities(identity.actor.capabilityAssignments)), + }; + const action = payload.actions?.[0]; + if (action?.action_id === "muster.view_in_muster") { + await db + .update(schema.slackInboxEvents) + .set({ status: "processed", processedAt: new Date() }) + .where(eq(schema.slackInboxEvents.id, row.inbox.id)); + return; + } + if (action?.action_id === "muster.cancel" && action.value) { + requireCapability(subject, "agents.cancel"); + const run = await new GovernedAgentHarness(db).read(subject, action.value); + const gateway = await fetch( + `${process.env.AGENT_GATEWAY_URL ?? "http://agent-gateway:3002"}/v1/runs/${encodeURIComponent(run.runId)}/cancel`, + { method: "POST", signal: AbortSignal.timeout(5_000) }, + ); + if (!gateway.ok) throw new Error("Agent runtime did not accept cancellation"); + await db + .update(schema.slackInboxEvents) + .set({ status: "processed", processedAt: new Date() }) + .where(eq(schema.slackInboxEvents.id, row.inbox.id)); + return; + } + if (action?.action_id === "muster.retry" && action.value) { + const [prior] = await db + .select({ run: schema.agentRuns, agent: schema.agentDefinitions }) + .from(schema.agentRuns) + .innerJoin( + schema.agentDefinitions, + and( + eq(schema.agentDefinitions.id, schema.agentRuns.agentId), + eq(schema.agentDefinitions.organisationId, row.inbox.organisationId), + ), + ) + .where( + and( + eq(schema.agentRuns.id, action.value), + eq(schema.agentRuns.organisationId, row.inbox.organisationId), + eq(schema.agentRuns.requestedByActorId, subject.actorId), + ), + ) + .limit(1); + const request = prior?.run.request as { humanRequest?: unknown } | undefined; + if (!prior || typeof request?.humanRequest !== "string") + throw new Error("Run retry is not available"); + await new GovernedAgentHarness(db).invoke( + subject, + { + agentKey: prior.agent.name, + input: { prompt: request.humanRequest }, + mode: "slack", + correlationId: `${row.inbox.eventId}:retry`, + }, + `slack:${row.installation.id}:${row.inbox.eventId}:retry`, + ); + await db + .update(schema.slackInboxEvents) + .set({ status: "processed", processedAt: new Date() }) + .where(eq(schema.slackInboxEvents.id, row.inbox.id)); + return; + } + const exposures = await db + .select({ exposure: schema.slackAgentExposures, agent: schema.agentDefinitions }) + .from(schema.slackAgentExposures) + .innerJoin( + schema.agentDefinitions, + and( + eq(schema.agentDefinitions.id, schema.slackAgentExposures.agentId), + eq(schema.agentDefinitions.organisationId, row.inbox.organisationId), + eq(schema.agentDefinitions.status, "active"), + eq(schema.agentDefinitions.killSwitch, false), + ), + ) + .where( + and( + eq(schema.slackAgentExposures.installationId, row.installation.id), + eq(schema.slackAgentExposures.enabled, true), + ), + ); + const text = slackText(event.text, 4_000); + const requested = text.match(/(?:\/muster|use)\s+([\w -]+)/i)?.[1]?.trim().toLowerCase(); + const direct = event.channel_type === "im"; + const eligible = exposures.filter(({ exposure }) => { + const allowed = Array.isArray(exposure.allowedChannelIds) + ? exposure.allowedChannelIds.includes(channelId) + : false; + return direct ? exposure.allowDirectMessages : allowed; + }); + const selected = + eligible.find(({ agent }) => agent.name.toLowerCase() === requested) ?? + eligible.find(({ exposure }) => exposure.isDefault) ?? + eligible[0]; + if (!selected) { + await db + .update(schema.slackInboxEvents) + .set({ status: "ignored", processedAt: new Date(), error: "agent_not_exposed" }) + .where(eq(schema.slackInboxEvents.id, row.inbox.id)); + return; + } + try { + const accepted = await new GovernedAgentHarness(db).invoke( + subject, + { + agentKey: selected.agent.name, + input: { prompt: text || "Continue the Slack thread." }, + mode: "slack", + correlationId: row.inbox.eventId, + }, + `slack:${row.installation.id}:${row.inbox.eventId}`, + ); + const token = (decryptConnectorPayload( + row.installation.encryptedBotToken, + encryptionKey(), + ) as { token: string }).token; + const posted = await slackApi(token, "chat.postMessage", { + channel: channelId, + ...(threadTs ? { thread_ts: threadTs } : {}), + text: `Muster: ${selected.agent.name} is active. Run queued.`, + blocks: resultBlocks(selected.agent.name, "queued", { + runId: accepted.runId, + progress: "Queued; status will update here.", + }), + }); + await db.transaction(async (tx) => { + await tx + .insert(schema.slackRunDeliveries) + .values({ + id: newId(), + organisationId: row.inbox.organisationId, + installationId: row.installation.id, + runId: accepted.runId, + inboxEventId: row.inbox.id, + channelId, + threadTs: threadTs ?? posted.ts ?? "", + progressMessageTs: posted.ts, + status: "queued", + lastProgress: { stage: "queued", percent: 0 }, + }) + .onConflictDoNothing(); + await tx + .update(schema.slackInboxEvents) + .set({ status: "processed", processedAt: new Date() }) + .where(eq(schema.slackInboxEvents.id, row.inbox.id)); + await appendAuditEvent(tx, { + organisationId: row.inbox.organisationId, + actorId: identity.actor.id, + actorType: "human", + action: "slack.agent.invoked", + targetType: "agent_run", + targetId: accepted.runId, + metadata: { installationId: row.installation.id, channelId, agentId: selected.agent.id }, + traceId: row.inbox.eventId, + }); + }); + } catch (error) { + await db + .update(schema.slackInboxEvents) + .set({ + status: "failed", + processedAt: new Date(), + error: redactObservationText(error instanceof Error ? error.message : "Slack event failed"), + }) + .where(eq(schema.slackInboxEvents.id, row.inbox.id)); + throw error; + } +} + +export async function deliverSlackRun(runId: string) { + const db = database(); + const deliveries = await db + .select({ + delivery: schema.slackRunDeliveries, + installation: schema.slackInstallations, + run: schema.agentRuns, + agent: schema.agentDefinitions, + }) + .from(schema.slackRunDeliveries) + .innerJoin( + schema.slackInstallations, + and( + eq(schema.slackInstallations.id, schema.slackRunDeliveries.installationId), + eq(schema.slackInstallations.organisationId, schema.slackRunDeliveries.organisationId), + ), + ) + .innerJoin( + schema.agentRuns, + and( + eq(schema.agentRuns.id, schema.slackRunDeliveries.runId), + eq(schema.agentRuns.organisationId, schema.slackRunDeliveries.organisationId), + ), + ) + .innerJoin( + schema.agentDefinitions, + and( + eq(schema.agentDefinitions.id, schema.agentRuns.agentId), + eq(schema.agentDefinitions.organisationId, schema.agentRuns.organisationId), + ), + ) + .where( + and( + eq(schema.slackRunDeliveries.runId, runId), + eq(schema.slackRunDeliveries.status, "queued"), + ), + ); + for (const row of deliveries) { + if (!["completed", "failed", "cancelled"].includes(row.run.status)) continue; + const token = (decryptConnectorPayload( + row.installation.encryptedBotToken, + encryptionKey(), + ) as { token: string }).token; + try { + if (row.delivery.progressMessageTs) + await slackApi(token, "chat.update", { + channel: row.delivery.channelId, + ts: row.delivery.progressMessageTs, + text: `Muster: ${row.agent.name} ${row.run.status}.`, + blocks: resultBlocks(row.agent.name, row.run.status, { + ...(row.run.structuredOutput && + typeof row.run.structuredOutput === "object" && + !Array.isArray(row.run.structuredOutput) + ? row.run.structuredOutput + : {}), + runId: row.run.id, + }), + }); + await db.transaction(async (tx) => { + await tx + .update(schema.slackRunDeliveries) + .set({ + status: "delivered", + resultMessageTs: row.delivery.progressMessageTs, + updatedAt: new Date(), + }) + .where(eq(schema.slackRunDeliveries.id, row.delivery.id)); + await tx + .update(schema.slackInstallations) + .set({ lastDeliveryAt: new Date(), lastError: null, updatedAt: new Date() }) + .where(eq(schema.slackInstallations.id, row.installation.id)); + }); + } catch (error) { + await db + .update(schema.slackRunDeliveries) + .set({ + attemptCount: row.delivery.attemptCount + 1, + lastError: redactObservationText(error instanceof Error ? error.message : "Slack delivery failed"), + updatedAt: new Date(), + }) + .where(eq(schema.slackRunDeliveries.id, row.delivery.id)); + throw error; + } + } +} + +export { decryptConnectorPayload, encryptConnectorPayload }; diff --git a/packages/agent-harness/tsconfig.json b/packages/agent-harness/tsconfig.json new file mode 100644 index 0000000..3569d92 --- /dev/null +++ b/packages/agent-harness/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 97274d9..2d7442d 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -228,6 +228,68 @@ export const AgentInvestigationJobSchema = z.object({ }); export type AgentInvestigationJob = z.infer; +// Portable agent harness contracts deliberately describe only callable +// capabilities. Prompts, credentials and connector configuration never leave +// the authoritative runtime. +export const AgentHarnessInvocationModeSchema = z.enum([ + "slack", + "hermes", + "mcp", + "cli", + "http", +]); +export type AgentHarnessInvocationMode = z.infer< + typeof AgentHarnessInvocationModeSchema +>; + +export const AgentHarnessManifestSchema = z.object({ + protocolVersion: z.literal("muster.agent-harness/v1"), + key: z.string().min(1).max(120), + version: z.string().min(1).max(120), + name: z.string().min(1).max(160), + description: z.string().min(1).max(2_000), + invocationModes: z.array(AgentHarnessInvocationModeSchema).min(1), + inputSchema: z.literal("muster.agent-harness.input/v1"), + outputSchema: z.string().min(1).max(160), + requiredCapabilities: z.array(z.string().min(1).max(160)), + approvalBehavior: z.enum(["none", "governed_actions"]), + lifecycle: z.enum(["active", "disabled"]), +}); +export type AgentHarnessManifest = z.infer; + +export const AgentHarnessInvokeSchema = z.object({ + agentKey: z.string().trim().min(1).max(120), + input: z.object({ + prompt: z.string().trim().min(1).max(4_000), + roomId: z.string().uuid().optional(), + investigationId: z.string().uuid().optional(), + taskId: z.string().uuid().optional(), + caseId: z.string().trim().min(1).max(200).optional(), + }), + mode: AgentHarnessInvocationModeSchema, + correlationId: z.string().trim().min(8).max(160).optional(), +}); +export type AgentHarnessInvoke = z.infer; + +export const AgentHarnessRunSchema = z.object({ + protocolVersion: z.literal("muster.agent-harness/v1"), + runId: z.string().uuid(), + status: z.enum([ + "queued", + "running", + "awaiting_approval", + "waiting_sources", + "completed", + "failed", + "cancelled", + ]), + agentKey: z.string().min(1).max(120), + correlationId: z.string().min(8).max(160), + duplicate: z.boolean(), + result: z.unknown().nullable(), +}); +export type AgentHarnessRun = z.infer; + export const queueNames = [ "muster-ingestion", "muster-integrations", diff --git a/packages/database/src/schema.ts b/packages/database/src/schema.ts index d69b4a7..5c91d01 100644 --- a/packages/database/src/schema.ts +++ b/packages/database/src/schema.ts @@ -1193,6 +1193,191 @@ export const agentRuns = pgTable( ], ); +// Slack remains an adapter: these records map external identities and delivery +// state to authoritative organisations, actors and agent runs. Tokens and raw +// event bodies are encrypted before persistence. +export const slackInstallations = pgTable( + "slack_installations", + { + id: uuid("id").primaryKey(), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + teamId: text("team_id").notNull(), + teamName: text("team_name"), + enterpriseId: text("enterprise_id"), + botUserId: text("bot_user_id"), + scopes: jsonb("scopes").notNull().default([]), + encryptedBotToken: text("encrypted_bot_token").notNull(), + encryptedAppToken: text("encrypted_app_token"), + status: text("status").notNull().default("active"), + installedByActorId: uuid("installed_by_actor_id") + .notNull() + .references(() => actors.id), + installedAt: timestamp("installed_at", { withTimezone: true }) + .defaultNow() + .notNull(), + lastHealthAt: timestamp("last_health_at", { withTimezone: true }), + lastDeliveryAt: timestamp("last_delivery_at", { withTimezone: true }), + lastError: text("last_error"), + revokedAt: timestamp("revoked_at", { withTimezone: true }), + ...timestamps, + }, + (table) => [ + uniqueIndex("slack_installations_team_unique").on(table.teamId), + index("slack_installations_org_status_idx").on( + table.organisationId, + table.status, + ), + ], +); + +export const slackIdentityMappings = pgTable( + "slack_identity_mappings", + { + id: uuid("id").primaryKey(), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + installationId: uuid("installation_id") + .notNull() + .references(() => slackInstallations.id), + slackUserId: text("slack_user_id").notNull(), + actorId: uuid("actor_id") + .notNull() + .references(() => actors.id), + status: text("status").notNull().default("active"), + createdByActorId: uuid("created_by_actor_id") + .notNull() + .references(() => actors.id), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + revokedAt: timestamp("revoked_at", { withTimezone: true }), + }, + (table) => [ + uniqueIndex("slack_identity_installation_user_unique").on( + table.installationId, + table.slackUserId, + ), + uniqueIndex("slack_identity_org_actor_unique").on( + table.organisationId, + table.actorId, + ), + ], +); + +export const slackAgentExposures = pgTable( + "slack_agent_exposures", + { + id: uuid("id").primaryKey(), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + installationId: uuid("installation_id") + .notNull() + .references(() => slackInstallations.id), + agentId: uuid("agent_id") + .notNull() + .references(() => agentDefinitions.id), + enabled: boolean("enabled").notNull().default(true), + isDefault: boolean("is_default").notNull().default(false), + allowedChannelIds: jsonb("allowed_channel_ids").notNull().default([]), + allowDirectMessages: boolean("allow_direct_messages").notNull().default(true), + allowThreadContext: boolean("allow_thread_context").notNull().default(false), + updatedByActorId: uuid("updated_by_actor_id") + .notNull() + .references(() => actors.id), + ...timestamps, + }, + (table) => [ + uniqueIndex("slack_exposures_installation_agent_unique").on( + table.installationId, + table.agentId, + ), + index("slack_exposures_org_enabled_idx").on( + table.organisationId, + table.enabled, + ), + ], +); + +export const slackInboxEvents = pgTable( + "slack_inbox_events", + { + id: uuid("id").primaryKey(), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + installationId: uuid("installation_id") + .notNull() + .references(() => slackInstallations.id), + eventId: text("event_id").notNull(), + eventType: text("event_type").notNull(), + payloadHash: text("payload_hash").notNull(), + encryptedPayload: text("encrypted_payload").notNull(), + status: text("status").notNull().default("queued"), + receivedAt: timestamp("received_at", { withTimezone: true }) + .defaultNow() + .notNull(), + processedAt: timestamp("processed_at", { withTimezone: true }), + error: text("error"), + }, + (table) => [ + uniqueIndex("slack_inbox_installation_event_unique").on( + table.installationId, + table.eventId, + ), + index("slack_inbox_org_status_idx").on( + table.organisationId, + table.status, + table.receivedAt, + ), + ], +); + +export const slackRunDeliveries = pgTable( + "slack_run_deliveries", + { + id: uuid("id").primaryKey(), + organisationId: uuid("organisation_id") + .notNull() + .references(() => organisations.id), + installationId: uuid("installation_id") + .notNull() + .references(() => slackInstallations.id), + runId: uuid("run_id") + .notNull() + .references(() => agentRuns.id), + inboxEventId: uuid("inbox_event_id").references(() => slackInboxEvents.id), + channelId: text("channel_id").notNull(), + threadTs: text("thread_ts").notNull(), + progressMessageTs: text("progress_message_ts"), + resultMessageTs: text("result_message_ts"), + status: text("status").notNull().default("queued"), + lastProgress: jsonb("last_progress").notNull().default({}), + attemptCount: integer("attempt_count").notNull().default(0), + lastError: text("last_error"), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }, + (table) => [ + uniqueIndex("slack_delivery_installation_run_unique").on( + table.installationId, + table.runId, + ), + index("slack_delivery_org_status_idx").on( + table.organisationId, + table.status, + table.updatedAt, + ), + ], +); + export const agentRunEvents = pgTable( "agent_run_events", { diff --git a/packages/database/src/verify-clean-install.ts b/packages/database/src/verify-clean-install.ts index 13dfb6a..15940e7 100644 --- a/packages/database/src/verify-clean-install.ts +++ b/packages/database/src/verify-clean-install.ts @@ -30,6 +30,11 @@ const operationalTables = { integrationConnectorCredentials: schema.integrationConnectorCredentials, integrationQueryTemplates: schema.integrationQueryTemplates, integrationQueryRuns: schema.integrationQueryRuns, + slackInstallations: schema.slackInstallations, + slackIdentityMappings: schema.slackIdentityMappings, + slackAgentExposures: schema.slackAgentExposures, + slackInboxEvents: schema.slackInboxEvents, + slackRunDeliveries: schema.slackRunDeliveries, huntRuns: schema.huntRuns, huntQueries: schema.huntQueries, researchWatchlists: schema.researchWatchlists, From 67ca15655d9967cb871a6a3802bbc39dea3c7c5d Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 11:09:35 +1000 Subject: [PATCH 26/47] feat: polish Slack agent harness lifecycle --- apps/agent-gateway/src/runtime.ts | 45 ++-- apps/web/app/api/v1/slack/install/route.ts | 24 ++- apps/web/app/settings/slack/page.tsx | 5 + apps/web/components/slack-settings-view.tsx | 88 ++++++++ apps/worker/src/index.ts | 2 +- docs/integrations/agent-harness.md | 22 +- packages/agent-harness/src/index.test.ts | 40 ++++ packages/agent-harness/src/index.ts | 217 ++++++++++++++++++-- 8 files changed, 406 insertions(+), 37 deletions(-) create mode 100644 apps/web/app/settings/slack/page.tsx create mode 100644 apps/web/components/slack-settings-view.tsx diff --git a/apps/agent-gateway/src/runtime.ts b/apps/agent-gateway/src/runtime.ts index a331563..cc49aa7 100644 --- a/apps/agent-gateway/src/runtime.ts +++ b/apps/agent-gateway/src/runtime.ts @@ -1188,21 +1188,36 @@ export class DurableAgentRuntime { private async heartbeat(runId: string) { const now = new Date(); - const updated = await database() - .update(schema.agentRuns) - .set({ - heartbeatAt: now, - leaseExpiresAt: new Date(now.getTime() + this.leaseMs), - progress: { stage: "executing", percent: 50 }, - }) - .where( - and( - eq(schema.agentRuns.id, runId), - eq(schema.agentRuns.status, "running"), - eq(schema.agentRuns.workerId, this.workerId), - ), - ) - .returning({ id: schema.agentRuns.id }); + const updated = await database().transaction(async (tx) => { + const rows = await tx + .update(schema.agentRuns) + .set({ + heartbeatAt: now, + leaseExpiresAt: new Date(now.getTime() + this.leaseMs), + progress: { stage: "executing", percent: 50 }, + }) + .where( + and( + eq(schema.agentRuns.id, runId), + eq(schema.agentRuns.status, "running"), + eq(schema.agentRuns.workerId, this.workerId), + ), + ) + .returning({ id: schema.agentRuns.id, organisationId: schema.agentRuns.organisationId }); + const run = rows[0]; + if (run) + await writeOutbox(tx, { + organisationId: run.organisationId, + eventType: "agent.run.progress", + aggregateType: "agent_run", + aggregateId: run.id, + queueName: "muster-notifications", + payload: { runId: run.id, stage: "executing", percent: 50 }, + idempotencyKey: `agent.run.progress:${run.id}:executing`, + traceId: `agent-run-${run.id}`, + }); + return rows; + }); if (updated.length === 0) this.activeRuns.get(runId)?.abort(); } diff --git a/apps/web/app/api/v1/slack/install/route.ts b/apps/web/app/api/v1/slack/install/route.ts index 7984866..f65a273 100644 --- a/apps/web/app/api/v1/slack/install/route.ts +++ b/apps/web/app/api/v1/slack/install/route.ts @@ -1,8 +1,14 @@ import { requireCapability } from "@muster/authz"; -import { signSlackOAuthState } from "@muster/agent-harness"; +import { SlackGovernanceAdapter, signSlackOAuthState } from "@muster/agent-harness"; import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; -const scopes = ["app_mentions:read", "chat:write", "commands", "im:history"]; +const scopes = [ + "app_mentions:read", + "assistant:write", + "chat:write", + "commands", + "im:history", +]; export async function GET(request: Request) { const traceId = requestTraceId(request); @@ -27,3 +33,17 @@ export async function GET(request: Request) { return problemResponse(error, traceId); } } + +export async function DELETE(request: Request) { + const traceId = requestTraceId(request); + try { + const subject = await apiSubject(request); + requireCapability(subject, "administration.manage"); + const installationId = new URL(request.url).searchParams.get("installationId"); + if (!installationId) throw new Error("Slack installation id is required"); + await new SlackGovernanceAdapter().revoke(subject, installationId); + return Response.json({ data: { status: "revoked" }, traceId }); + } catch (error) { + return problemResponse(error, traceId); + } +} diff --git a/apps/web/app/settings/slack/page.tsx b/apps/web/app/settings/slack/page.tsx new file mode 100644 index 0000000..37dcd6b --- /dev/null +++ b/apps/web/app/settings/slack/page.tsx @@ -0,0 +1,5 @@ +import { SlackSettingsView } from "@/components/slack-settings-view"; + +export default function SlackSettingsPage() { + return ; +} diff --git a/apps/web/components/slack-settings-view.tsx b/apps/web/components/slack-settings-view.tsx new file mode 100644 index 0000000..290307e --- /dev/null +++ b/apps/web/components/slack-settings-view.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { AppShell } from "@/components/app-shell"; +import { PageHeader } from "@/components/page-header"; +import { Button } from "@/components/ui/button"; + +type SlackInstallation = { + id: string; + teamId: string; + teamName: string | null; + status: string; + lastHealthAt: string | null; + lastDeliveryAt: string | null; + lastError: string | null; +}; + +export function SlackSettingsView() { + const [installations, setInstallations] = useState([]); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + const load = useCallback(async () => { + setLoading(true); + try { + const response = await fetch("/api/v1/slack/health", { cache: "no-store" }); + const payload = (await response.json()) as { data?: SlackInstallation[]; error?: string }; + if (!response.ok) throw new Error(payload.error ?? "Slack health check failed"); + setInstallations(payload.data ?? []); + setError(null); + } catch (cause) { + setError(cause instanceof Error ? cause.message : "Slack health check failed"); + } finally { + setLoading(false); + } + }, []); + useEffect(() => void load(), [load]); + + const reconnect = async () => { + const response = await fetch("/api/v1/slack/install", { cache: "no-store" }); + const payload = (await response.json()) as { data?: { authorizationUrl?: string }; error?: string }; + if (!response.ok || !payload.data?.authorizationUrl) { + setError(payload.error ?? "Could not start Slack OAuth"); + return; + } + window.location.assign(payload.data.authorizationUrl); + }; + + const revoke = async (installationId: string) => { + const response = await fetch( + `/api/v1/slack/install?installationId=${encodeURIComponent(installationId)}`, + { method: "DELETE" }, + ); + if (!response.ok) setError("Could not revoke Slack installation"); + await load(); + }; + + return ( + + void reconnect()}>Connect or reconnect Slack} + /> +
+ {error ?

{error}

: null} + {loading ?

Checking Slack health…

: null} + {!loading && installations.length === 0 ? ( +

No Slack workspace is connected.

+ ) : null} +
+ {installations.map((installation) => ( +
+
+

{installation.teamName ?? installation.teamId}

+

+ {installation.status} · health {installation.lastHealthAt ?? "not checked"} · delivery {installation.lastDeliveryAt ?? "none"} +

+ {installation.lastError ?

{installation.lastError}

: null} +
+ +
+ ))} +
+
+
+ ); +} diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 7ab1490..bd0925f 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -143,7 +143,7 @@ const authoritativeProcessor: Processor = async (job) => { } if ( job.queueName === "muster-notifications" && - job.name === "agent.run.settled" + (job.name === "agent.run.settled" || job.name === "agent.run.progress") ) { await deliverSlackRun(job.data.aggregateId); } diff --git a/docs/integrations/agent-harness.md b/docs/integrations/agent-harness.md index 9e1d99b..131967b 100644 --- a/docs/integrations/agent-harness.md +++ b/docs/integrations/agent-harness.md @@ -22,6 +22,10 @@ actor may invoke. Submit `POST /api/v1/agent-harness/invocations` with an Poll `GET /api/v1/agent-harness/runs/:id`; `DELETE` on that resource requests a capability-checked cancellation. Hermes, MCP, and CLI adapters use the same manifest and invocation shape, changing only `mode` to `hermes`, `mcp`, or `cli`. +For example, Hermes submits the signed actor's `agentKey`, structured input, and +`mode: "hermes"`; an MCP server exposes `agents/list`, `agents/invoke`, +`agents/get`, and `agents/cancel` as thin calls to these routes. A CLI uses the +same HTTP contract and must supply a new idempotency key per logical request. Adapters must never pass prompts, connector tokens, or restricted evidence to a different tenant. @@ -40,7 +44,8 @@ Configure Slack as follows: - Events URL: `/api/v1/slack/events` - Interactivity URL: `/api/v1/slack/interactions` - Slash command URL: `/api/v1/slack/commands` -- Bot scopes: `app_mentions:read`, `chat:write`, `commands`, and `im:history` +- Bot scopes: `app_mentions:read`, `assistant:write`, `chat:write`, `commands`, + and `im:history` - Subscribe to app mentions and direct messages; add the Slack Assistant event subscriptions when Assistant Threads are enabled for the workspace. @@ -48,11 +53,24 @@ Administrators map each Slack user to an active Muster human actor with `POST /api/v1/slack/identities`, then expose approved agents and channel policy using `PUT /api/v1/slack/exposures`. `GET /api/v1/slack/health` is the admin health view for installation status, scopes, delivery time, and redacted errors. +The default exposed agent handles ordinary mentions and DMs; users can explicitly +switch with `/muster AgentName …`, `use AgentName …`, or a slash-command argument +beginning with the exposed agent name. Slack accepts only fresh HMAC-signed requests, persists them using a workspace event idempotency key, acknowledges immediately, and invokes agents asynchronously -from the outbox. Result messages are updated in their original thread and offer +from the outbox. One bounded execution-progress update and the terminal typed +result are updated in the original thread and offer capability-checked Cancel, Retry, and View in Muster actions. +Slack Assistant lifecycle events `assistant_thread_started` and +`assistant_thread_context_changed` start a governed run in the assistant DM +thread and use `assistant.threads.setStatus` for queued and terminal status. +Thread context is retained only when that exposed agent explicitly allows it. +Reconnecting repeats OAuth and atomically rotates the encrypted bot token. An +administrator can revoke an installation with `DELETE /api/v1/slack/install` +and `installationId`; this revokes Slack access and cryptographically replaces +the locally stored token. + For Socket Mode, acknowledge Slack's `envelope_id` immediately and pass the envelope payload to `SlackGovernanceAdapter.recordSocketEnvelope`. It shares the same encrypted inbox and idempotency path as Events API delivery, so reconnects diff --git a/packages/agent-harness/src/index.test.ts b/packages/agent-harness/src/index.test.ts index 301f32a..42104b1 100644 --- a/packages/agent-harness/src/index.test.ts +++ b/packages/agent-harness/src/index.test.ts @@ -1,6 +1,8 @@ import { createHmac } from "node:crypto"; import { describe, expect, it } from "vitest"; import { + normaliseSlackConversation, + slackResultBlocks, signSlackOAuthState, verifySlackOAuthState, verifySlackRequest, @@ -35,4 +37,42 @@ describe("Slack governed harness boundary", () => { if (original === undefined) delete process.env.SLACK_OAUTH_STATE_SECRET; else process.env.SLACK_OAUTH_STATE_SECRET = original; }); + + it("normalises Slack Assistant lifecycle context without trusting its text", () => { + const conversation = normaliseSlackConversation({ + event: { + type: "assistant_thread_context_changed", + assistant_thread: { + user_id: "U123", + channel_id: "D123", + thread_ts: "1729999327.187299", + context: { channel_id: "C456", team_id: "T789" }, + }, + }, + }); + expect(conversation.slackUserId).toBe("U123"); + expect(conversation.channelId).toBe("D123"); + expect(conversation.threadTs).toBe("1729999327.187299"); + expect(conversation.assistantThread?.context?.channel_id).toBe("C456"); + }); + + it("renders bounded typed results with governed Slack actions", () => { + const queued = slackResultBlocks("Jessie", "queued", { runId: "run-1" }); + const queuedActions = queued.find((block) => block.type === "actions") as { + elements: Array<{ action_id: string }>; + }; + expect(queuedActions.elements.map((element) => element.action_id)).toContain("muster.cancel"); + + const failed = slackResultBlocks("Jessie", "failed", { + runId: "run-1", + summary: "Bounded synthetic failure", + confidence: 0.8, + gaps: ["No connector evidence"], + }); + const failedActions = failed.find((block) => block.type === "actions") as { + elements: Array<{ action_id: string }>; + }; + expect(failedActions.elements.map((element) => element.action_id)).toContain("muster.retry"); + expect(JSON.stringify(failed)).not.toContain("connector-token"); + }); }); diff --git a/packages/agent-harness/src/index.ts b/packages/agent-harness/src/index.ts index 2a8e42b..a67622b 100644 --- a/packages/agent-harness/src/index.ts +++ b/packages/agent-harness/src/index.ts @@ -510,6 +510,53 @@ export class SlackGovernanceAdapter { async health(subject: AuthorisationSubject) { requireCapability(subject, "administration.manage"); + const installations = await this.db + .select({ + id: schema.slackInstallations.id, + teamId: schema.slackInstallations.teamId, + teamName: schema.slackInstallations.teamName, + scopes: schema.slackInstallations.scopes, + status: schema.slackInstallations.status, + lastHealthAt: schema.slackInstallations.lastHealthAt, + lastDeliveryAt: schema.slackInstallations.lastDeliveryAt, + lastError: schema.slackInstallations.lastError, + }) + .from(schema.slackInstallations) + .where(eq(schema.slackInstallations.organisationId, subject.organisationId)); + await Promise.all( + installations + .filter((installation) => installation.status === "active") + .map(async (installation) => { + try { + const token = (decryptConnectorPayload( + ( + await this.db + .select({ encryptedBotToken: schema.slackInstallations.encryptedBotToken }) + .from(schema.slackInstallations) + .where(eq(schema.slackInstallations.id, installation.id)) + .limit(1) + )[0]?.encryptedBotToken ?? "", + encryptionKey(), + ) as { token: string }).token; + await slackApi(token, "auth.test", {}); + await this.db + .update(schema.slackInstallations) + .set({ lastHealthAt: new Date(), lastError: null, updatedAt: new Date() }) + .where(eq(schema.slackInstallations.id, installation.id)); + } catch (error) { + await this.db + .update(schema.slackInstallations) + .set({ + lastHealthAt: new Date(), + lastError: redactObservationText( + error instanceof Error ? error.message : "Slack health check failed", + ), + updatedAt: new Date(), + }) + .where(eq(schema.slackInstallations.id, installation.id)); + } + }), + ); return this.db .select({ id: schema.slackInstallations.id, @@ -525,6 +572,53 @@ export class SlackGovernanceAdapter { .where(eq(schema.slackInstallations.organisationId, subject.organisationId)); } + async revoke(subject: AuthorisationSubject, installationId: string) { + requireCapability(subject, "administration.manage"); + const [installation] = await this.db + .select() + .from(schema.slackInstallations) + .where( + and( + eq(schema.slackInstallations.id, installationId), + eq(schema.slackInstallations.organisationId, subject.organisationId), + ), + ) + .limit(1); + if (!installation) throw new Error("Slack installation not found"); + try { + const token = (decryptConnectorPayload( + installation.encryptedBotToken, + encryptionKey(), + ) as { token: string }).token; + await slackApi(token, "auth.revoke", { test: false }); + } finally { + await this.db.transaction(async (tx) => { + await tx + .update(schema.slackInstallations) + .set({ + status: "revoked", + revokedAt: new Date(), + encryptedBotToken: encryptConnectorPayload( + { revoked: true }, + encryptionKey(), + ), + updatedAt: new Date(), + }) + .where(eq(schema.slackInstallations.id, installation.id)); + await appendAuditEvent(tx, { + organisationId: subject.organisationId, + actorId: subject.actorId, + actorType: "human", + action: "slack.installation.revoked", + targetType: "slack_installation", + targetId: installation.id, + metadata: { teamId: installation.teamId }, + traceId: crypto.randomUUID(), + }); + }); + } + } + async mapIdentity( subject: AuthorisationSubject, input: { installationId: string; slackUserId: string; actorId: string }, @@ -683,6 +777,7 @@ type SlackMessage = { text?: string; thread_ts?: string; ts?: string; + assistant_thread?: SlackAssistantThread; }; type SlackEnvelope = { @@ -695,6 +790,36 @@ type SlackEnvelope = { actions?: Array<{ action_id?: string; value?: string }>; }; +type SlackAssistantThread = { + user_id?: string; + channel_id?: string; + thread_ts?: string; + context?: { channel_id?: string; team_id?: string; enterprise_id?: string }; +}; + +export function normaliseSlackConversation(payload: SlackEnvelope) { + const event: SlackMessage = payload.event ?? {}; + const assistantThread = + event.type === "assistant_thread_started" || + event.type === "assistant_thread_context_changed" + ? (event.assistant_thread ?? null) + : null; + return { + event, + assistantThread, + slackUserId: event.user ?? assistantThread?.user_id ?? payload.user?.id, + channelId: event.channel ?? assistantThread?.channel_id ?? payload.channel?.id, + threadTs: + event.thread_ts ?? + assistantThread?.thread_ts ?? + payload.message?.thread_ts ?? + payload.container?.thread_ts ?? + event.ts ?? + payload.message?.ts ?? + payload.container?.message_ts, + }; +} + function slackText(value: unknown, max = 2_000) { return redactObservationText(typeof value === "string" ? value : "", { maxStringLength: max, @@ -717,7 +842,7 @@ async function slackApi(token: string, method: string, body: Record) @@ -803,16 +928,8 @@ export async function processSlackInboxEvent(inboxEventId: string) { row.inbox.encryptedPayload, encryptionKey(), ) as SlackEnvelope; - const event: SlackMessage = payload.event ?? {}; - const slackUserId = event.user ?? payload.user?.id; - const channelId = event.channel ?? payload.channel?.id; - const threadTs = - event.thread_ts ?? - payload.message?.thread_ts ?? - payload.container?.thread_ts ?? - event.ts ?? - payload.message?.ts ?? - payload.container?.message_ts; + const { event, assistantThread, slackUserId, channelId, threadTs } = + normaliseSlackConversation(payload); if (!slackUserId || !channelId) { await db .update(schema.slackInboxEvents) @@ -931,7 +1048,7 @@ export async function processSlackInboxEvent(inboxEventId: string) { ); const text = slackText(event.text, 4_000); const requested = text.match(/(?:\/muster|use)\s+([\w -]+)/i)?.[1]?.trim().toLowerCase(); - const direct = event.channel_type === "im"; + const direct = event.channel_type === "im" || Boolean(assistantThread); const eligible = exposures.filter(({ exposure }) => { const allowed = Array.isArray(exposure.allowedChannelIds) ? exposure.allowedChannelIds.includes(channelId) @@ -940,6 +1057,9 @@ export async function processSlackInboxEvent(inboxEventId: string) { }); const selected = eligible.find(({ agent }) => agent.name.toLowerCase() === requested) ?? + eligible.find(({ agent }) => + text.toLowerCase().startsWith(`${agent.name.toLowerCase()} `), + ) ?? eligible.find(({ exposure }) => exposure.isDefault) ?? eligible[0]; if (!selected) { @@ -954,7 +1074,13 @@ export async function processSlackInboxEvent(inboxEventId: string) { subject, { agentKey: selected.agent.name, - input: { prompt: text || "Continue the Slack thread." }, + input: { + prompt: + text || + (assistantThread + ? "Assist the user in this Slack Assistant thread. Keep the response bounded and governed." + : "Continue the Slack thread."), + }, mode: "slack", correlationId: row.inbox.eventId, }, @@ -968,11 +1094,18 @@ export async function processSlackInboxEvent(inboxEventId: string) { channel: channelId, ...(threadTs ? { thread_ts: threadTs } : {}), text: `Muster: ${selected.agent.name} is active. Run queued.`, - blocks: resultBlocks(selected.agent.name, "queued", { + blocks: slackResultBlocks(selected.agent.name, "queued", { runId: accepted.runId, progress: "Queued; status will update here.", }), }); + if (assistantThread) { + await slackApi(token, "assistant.threads.setStatus", { + channel_id: channelId, + thread_ts: threadTs, + status: "Muster is preparing a governed response…", + }).catch(() => undefined); + } await db.transaction(async (tx) => { await tx .insert(schema.slackRunDeliveries) @@ -986,7 +1119,14 @@ export async function processSlackInboxEvent(inboxEventId: string) { threadTs: threadTs ?? posted.ts ?? "", progressMessageTs: posted.ts, status: "queued", - lastProgress: { stage: "queued", percent: 0 }, + lastProgress: { + stage: "queued", + percent: 0, + assistantThread: Boolean(assistantThread), + contextChannelId: selected.exposure.allowThreadContext + ? assistantThread?.context?.channel_id + : undefined, + }, }) .onConflictDoNothing(); await tx @@ -1055,18 +1195,61 @@ export async function deliverSlackRun(runId: string) { ), ); for (const row of deliveries) { - if (!["completed", "failed", "cancelled"].includes(row.run.status)) continue; + const terminal = ["completed", "failed", "cancelled"].includes(row.run.status); const token = (decryptConnectorPayload( row.installation.encryptedBotToken, encryptionKey(), ) as { token: string }).token; try { + const progress = + row.delivery.lastProgress && + typeof row.delivery.lastProgress === "object" && + !Array.isArray(row.delivery.lastProgress) + ? (row.delivery.lastProgress as Record) + : {}; + if (!terminal) { + const runProgress = + row.run.progress && + typeof row.run.progress === "object" && + !Array.isArray(row.run.progress) + ? (row.run.progress as Record) + : {}; + const stage = slackText(runProgress.stage, 120) || "working"; + if (progress.stage === stage) continue; + if (row.delivery.progressMessageTs) + await slackApi(token, "chat.update", { + channel: row.delivery.channelId, + ts: row.delivery.progressMessageTs, + text: `Muster: ${row.agent.name} is ${stage}.`, + blocks: slackResultBlocks(row.agent.name, row.run.status, { + runId: row.run.id, + summary: `Progress: ${stage}.`, + }), + }); + await db + .update(schema.slackRunDeliveries) + .set({ + lastProgress: { ...progress, stage }, + updatedAt: new Date(), + }) + .where(eq(schema.slackRunDeliveries.id, row.delivery.id)); + continue; + } + if (progress.assistantThread === true) + await slackApi(token, "assistant.threads.setStatus", { + channel_id: row.delivery.channelId, + thread_ts: row.delivery.threadTs, + status: + row.run.status === "completed" + ? "Muster completed the governed response." + : `Muster ${row.run.status} the governed response.`, + }); if (row.delivery.progressMessageTs) await slackApi(token, "chat.update", { channel: row.delivery.channelId, ts: row.delivery.progressMessageTs, text: `Muster: ${row.agent.name} ${row.run.status}.`, - blocks: resultBlocks(row.agent.name, row.run.status, { + blocks: slackResultBlocks(row.agent.name, row.run.status, { ...(row.run.structuredOutput && typeof row.run.structuredOutput === "object" && !Array.isArray(row.run.structuredOutput) From 8cfb1de84e43aa11ebe62177b65ba601d5e1a3ff Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 11:13:27 +1000 Subject: [PATCH 27/47] test: cover governed Slack harness flow --- docs/integrations/agent-harness.md | 20 +- packages/agent-harness/src/index.test.ts | 15 ++ packages/agent-harness/src/index.ts | 38 +++ .../src/slack.integration.test.ts | 252 ++++++++++++++++++ 4 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 packages/agent-harness/src/slack.integration.test.ts diff --git a/docs/integrations/agent-harness.md b/docs/integrations/agent-harness.md index 131967b..c34a878 100644 --- a/docs/integrations/agent-harness.md +++ b/docs/integrations/agent-harness.md @@ -60,7 +60,10 @@ Slack accepts only fresh HMAC-signed requests, persists them using a workspace event idempotency key, acknowledges immediately, and invokes agents asynchronously from the outbox. One bounded execution-progress update and the terminal typed result are updated in the original thread and offer -capability-checked Cancel, Retry, and View in Muster actions. +capability-checked Cancel, Retry, Approval Review, and View in Muster actions. +Approval Review verifies the mapped approver and opens the authoritative Muster +approval record; only the existing Muster approval decision flow can approve or +execute a dangerous action. Slack Assistant lifecycle events `assistant_thread_started` and `assistant_thread_context_changed` start a governed run in the assistant DM @@ -75,3 +78,18 @@ For Socket Mode, acknowledge Slack's `envelope_id` immediately and pass the envelope payload to `SlackGovernanceAdapter.recordSocketEnvelope`. It shares the same encrypted inbox and idempotency path as Events API delivery, so reconnects cannot duplicate a run. + +## Verification + +The fast harness suite verifies signature replay handling, Assistant lifecycle +normalisation, typed Block Kit actions, and the portable adapter contract. The +database-backed deterministic Slack flow is opt-in because it uses the local +synthetic PostgreSQL fixture only: + +```sh +MUSTER_INTEGRATION_TESTS=true pnpm --dir packages/agent-harness test +``` + +It asserts signed event replay, an exact-once inbox/run, bounded progress and +terminal updates, approval review, cancel/retry, Assistant status lifecycle, and +out-of-order terminal delivery without Slack credentials. diff --git a/packages/agent-harness/src/index.test.ts b/packages/agent-harness/src/index.test.ts index 42104b1..077c395 100644 --- a/packages/agent-harness/src/index.test.ts +++ b/packages/agent-harness/src/index.test.ts @@ -1,5 +1,6 @@ import { createHmac } from "node:crypto"; import { describe, expect, it } from "vitest"; +import { AgentHarnessInvokeSchema } from "@muster/contracts"; import { normaliseSlackConversation, slackResultBlocks, @@ -68,11 +69,25 @@ describe("Slack governed harness boundary", () => { summary: "Bounded synthetic failure", confidence: 0.8, gaps: ["No connector evidence"], + approvalId: "00000000-0000-4000-8000-000000000003", }); const failedActions = failed.find((block) => block.type === "actions") as { elements: Array<{ action_id: string }>; }; expect(failedActions.elements.map((element) => element.action_id)).toContain("muster.retry"); + expect(failedActions.elements.map((element) => element.action_id)).toContain("muster.approval.view"); expect(JSON.stringify(failed)).not.toContain("connector-token"); }); + + it("keeps Hermes, MCP, CLI, and HTTP invocations on one portable contract", () => { + for (const mode of ["hermes", "mcp", "cli", "http"] as const) { + expect( + AgentHarnessInvokeSchema.parse({ + agentKey: "Synthetic Agent", + mode, + input: { prompt: "Synthetic bounded request" }, + }).mode, + ).toBe(mode); + } + }); }); diff --git a/packages/agent-harness/src/index.ts b/packages/agent-harness/src/index.ts index a67622b..0dd9887 100644 --- a/packages/agent-harness/src/index.ts +++ b/packages/agent-harness/src/index.ts @@ -884,6 +884,13 @@ export function slackResultBlocks(agentName: string, status: string, output: unk text: { type: "plain_text", text: "Retry" }, value: result.runId, }); + if (typeof result.approvalId === "string") + actions.push({ + type: "button", + action_id: "muster.approval.view", + text: { type: "plain_text", text: "Review approval" }, + value: result.approvalId, + }); return [ { type: "section", @@ -976,6 +983,37 @@ export async function processSlackInboxEvent(inboxEventId: string) { .where(eq(schema.slackInboxEvents.id, row.inbox.id)); return; } + if (action?.action_id === "muster.approval.view" && action.value) { + requireCapability(subject, "workflows.approve"); + const [approval] = await db + .select({ id: schema.approvals.id }) + .from(schema.approvals) + .where( + and( + eq(schema.approvals.id, action.value), + eq(schema.approvals.organisationId, row.inbox.organisationId), + ), + ) + .limit(1); + if (!approval) throw new Error("Approval not found"); + await db.transaction(async (tx) => { + await tx + .update(schema.slackInboxEvents) + .set({ status: "processed", processedAt: new Date() }) + .where(eq(schema.slackInboxEvents.id, row.inbox.id)); + await appendAuditEvent(tx, { + organisationId: row.inbox.organisationId, + actorId: subject.actorId, + actorType: "human", + action: "slack.approval.review.opened", + targetType: "approval", + targetId: approval.id, + metadata: { installationId: row.installation.id }, + traceId: row.inbox.eventId, + }); + }); + return; + } if (action?.action_id === "muster.cancel" && action.value) { requireCapability(subject, "agents.cancel"); const run = await new GovernedAgentHarness(db).read(subject, action.value); diff --git a/packages/agent-harness/src/slack.integration.test.ts b/packages/agent-harness/src/slack.integration.test.ts new file mode 100644 index 0000000..0222a67 --- /dev/null +++ b/packages/agent-harness/src/slack.integration.test.ts @@ -0,0 +1,252 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { closeDatabase, database, newId, schema } from "@muster/database"; +import { and, eq, inArray, like } from "drizzle-orm"; +import { + deliverSlackRun, + encryptConnectorPayload, + processSlackInboxEvent, + SlackGovernanceAdapter, +} from "./index"; + +const integration = process.env.MUSTER_INTEGRATION_TESTS === "true"; +const describeIntegration = integration ? describe.sequential : describe.skip; + +describeIntegration("synthetic Slack governed-agent delivery", () => { + const db = database(); + const suffix = newId(); + const eventId = `Ev-synthetic-${suffix}`; + const teamId = `T-synthetic-${suffix}`; + const slackUserId = `U-synthetic-${suffix}`; + let organisationId = ""; + let actorId = ""; + let agentId = ""; + let installationId = ""; + let runId = ""; + let approvalId = ""; + const posted: Array<{ method: string; body: Record }> = []; + const originalFetch = globalThis.fetch; + + beforeAll(async () => { + process.env.CONNECTOR_ENCRYPTION_KEY = Buffer.alloc(32, 23).toString("base64"); + const actors = await db + .select({ + id: schema.actors.id, + organisationId: schema.actors.organisationId, + capabilities: schema.actors.capabilityAssignments, + }) + .from(schema.actors) + .where(eq(schema.actors.actorType, "human")); + const actor = actors.find( + (candidate) => + Array.isArray(candidate.capabilities) && + candidate.capabilities.includes("agents.invoke") && + candidate.capabilities.includes("agents.cancel") && + candidate.capabilities.includes("workflows.approve"), + ); + if (!actor) throw new Error("Bootstrap a synthetic Muster workspace before integration tests"); + organisationId = actor.organisationId; + actorId = actor.id; + const [agent] = await db + .select({ id: schema.agentDefinitions.id }) + .from(schema.agentDefinitions) + .where( + and( + eq(schema.agentDefinitions.organisationId, organisationId), + eq(schema.agentDefinitions.status, "active"), + eq(schema.agentDefinitions.killSwitch, false), + ), + ) + .limit(1); + if (!agent) throw new Error("Bootstrap an active synthetic agent before integration tests"); + agentId = agent.id; + installationId = newId(); + await db.insert(schema.slackInstallations).values({ + id: installationId, + organisationId, + teamId, + teamName: "Synthetic Slack", + encryptedBotToken: encryptConnectorPayload( + { token: "xoxb-synthetic" }, + process.env.CONNECTOR_ENCRYPTION_KEY, + ), + installedByActorId: actorId, + }); + await db.insert(schema.slackIdentityMappings).values({ + id: newId(), + organisationId, + installationId, + slackUserId, + actorId, + createdByActorId: actorId, + }); + await db.insert(schema.slackAgentExposures).values({ + id: newId(), + organisationId, + installationId, + agentId, + isDefault: true, + allowedChannelIds: ["C-synthetic"], + allowDirectMessages: true, + allowThreadContext: true, + updatedByActorId: actorId, + }); + approvalId = newId(); + await db.insert(schema.approvals).values({ + id: approvalId, + organisationId, + requestingActorId: actorId, + actionType: "synthetic.slack.review", + target: { synthetic: true }, + riskSummary: "Synthetic Slack approval review fixture", + expiresAt: new Date(Date.now() + 60_000), + requiredCapability: "workflows.approve", + idempotencyKey: `synthetic-slack-approval:${suffix}`, + }); + globalThis.fetch = vi.fn(async (input, init) => { + const method = new URL(String(input)).pathname.split("/").pop() ?? "unknown"; + posted.push({ + method, + body: JSON.parse(String(init?.body ?? "{}")) as Record, + }); + return Response.json({ ok: true, ts: `1710000000.${posted.length}` }); + }) as typeof fetch; + }); + + afterAll(async () => { + globalThis.fetch = originalFetch; + const runs = await db + .select({ id: schema.agentRuns.id }) + .from(schema.agentRuns) + .where(like(schema.agentRuns.idempotencyKey, `slack:${installationId}:%`)); + const runIds = runs.map((run) => run.id); + await db.delete(schema.slackRunDeliveries).where(eq(schema.slackRunDeliveries.installationId, installationId)); + await db.delete(schema.slackInboxEvents).where(eq(schema.slackInboxEvents.installationId, installationId)); + if (runIds.length) { + await db.delete(schema.agentRunEvents).where(inArray(schema.agentRunEvents.runId, runIds)); + await db.delete(schema.agentRuns).where(inArray(schema.agentRuns.id, runIds)); + } + await db.delete(schema.slackAgentExposures).where(eq(schema.slackAgentExposures.installationId, installationId)); + await db.delete(schema.slackIdentityMappings).where(eq(schema.slackIdentityMappings.installationId, installationId)); + await db.delete(schema.slackInstallations).where(eq(schema.slackInstallations.id, installationId)); + await db.delete(schema.approvals).where(eq(schema.approvals.id, approvalId)); + await closeDatabase(); + }); + + it("persists a signed event once, invokes once, and updates bounded progress then terminal result", async () => { + const adapter = new SlackGovernanceAdapter(db); + const payload = { + type: "event_callback", + team_id: teamId, + event_id: eventId, + event: { + type: "app_mention", + user: slackUserId, + channel: "C-synthetic", + ts: "1710000000.000100", + text: "use default synthetic agent", + }, + }; + const raw = JSON.stringify(payload); + const first = await adapter.recordEvent(raw, payload); + const replay = await adapter.recordEvent(raw, payload); + expect(first.duplicate).toBe(false); + expect(replay.duplicate).toBe(true); + expect(first.inboxEvent?.id).toBeTruthy(); + + await processSlackInboxEvent(first.inboxEvent!.id); + await processSlackInboxEvent(first.inboxEvent!.id); + const [run] = await db + .select() + .from(schema.agentRuns) + .where(eq(schema.agentRuns.idempotencyKey, `slack:${installationId}:${eventId}`)) + .limit(1); + expect(run).toBeTruthy(); + runId = run!.id; + expect(posted.filter((call) => call.method === "chat.postMessage")).toHaveLength(1); + + await db + .update(schema.agentRuns) + .set({ status: "running", progress: { stage: "executing", percent: 50 } }) + .where(eq(schema.agentRuns.id, runId)); + await deliverSlackRun(runId); + await db + .update(schema.agentRuns) + .set({ + status: "completed", + progress: { stage: "completed", percent: 100 }, + structuredOutput: { + summary: "Synthetic typed completion", + confidence: 0.9, + gaps: ["Synthetic fixture only"], + approvalId, + }, + }) + .where(eq(schema.agentRuns.id, runId)); + await deliverSlackRun(runId); + const updates = posted.filter((call) => call.method === "chat.update"); + expect(updates).toHaveLength(2); + expect(JSON.stringify(updates.at(-1)?.body.blocks)).toContain("Synthetic typed completion"); + await deliverSlackRun(runId); + expect(posted.filter((call) => call.method === "chat.update")).toHaveLength(2); + }); + + it("handles cancel/retry actions and Assistant lifecycle out of order without leaking tenants", async () => { + const adapter = new SlackGovernanceAdapter(db); + const cancelPayload = { + type: "block_actions", + team: { id: teamId }, + user: { id: slackUserId }, + channel: { id: "C-synthetic" }, + message: { ts: "1710000000.000100" }, + actions: [{ action_id: "muster.cancel", value: runId }], + }; + const cancel = await adapter.recordEvent(JSON.stringify(cancelPayload), cancelPayload); + await processSlackInboxEvent(cancel.inboxEvent!.id); + expect(posted.some((call) => call.method === "cancel")).toBe(true); + + const approvalPayload = { + ...cancelPayload, + actions: [{ action_id: "muster.approval.view", value: approvalId }], + }; + const approval = await adapter.recordEvent( + `${JSON.stringify(approvalPayload)}-approval`, + approvalPayload, + ); + await processSlackInboxEvent(approval.inboxEvent!.id); + const [approvalInbox] = await db + .select({ status: schema.slackInboxEvents.status }) + .from(schema.slackInboxEvents) + .where(eq(schema.slackInboxEvents.id, approval.inboxEvent!.id)); + expect(approvalInbox?.status).toBe("processed"); + + const retryPayload = { + ...cancelPayload, + actions: [{ action_id: "muster.retry", value: runId }], + }; + const retry = await adapter.recordEvent(`${JSON.stringify(retryPayload)}-retry`, retryPayload); + await processSlackInboxEvent(retry.inboxEvent!.id); + const retryRuns = await db + .select({ id: schema.agentRuns.id }) + .from(schema.agentRuns) + .where(like(schema.agentRuns.idempotencyKey, `slack:${installationId}:%:retry`)); + expect(retryRuns).toHaveLength(1); + + const assistantPayload = { + type: "event_callback", + team_id: teamId, + event_id: `Ev-assistant-${suffix}`, + event: { + type: "assistant_thread_started", + assistant_thread: { + user_id: slackUserId, + channel_id: "D-synthetic", + thread_ts: "1710000001.000100", + context: { channel_id: "C-synthetic", team_id: teamId }, + }, + }, + }; + const assistant = await adapter.recordEvent(JSON.stringify(assistantPayload), assistantPayload); + await processSlackInboxEvent(assistant.inboxEvent!.id); + expect(posted.some((call) => call.method === "assistant.threads.setStatus")).toBe(true); + }); +}); From 27a086075330ad43b821d78d9b95f51b51404291 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 11:18:39 +1000 Subject: [PATCH 28/47] fix(slack): harden governed ingress boundaries --- .../app/api/v1/slack/oauth/callback/route.ts | 4 +- packages/agent-harness/src/index.test.ts | 6 + packages/agent-harness/src/index.ts | 117 ++++++++++++------ .../src/slack.integration.test.ts | 21 ++++ 4 files changed, 111 insertions(+), 37 deletions(-) diff --git a/apps/web/app/api/v1/slack/oauth/callback/route.ts b/apps/web/app/api/v1/slack/oauth/callback/route.ts index 6c53077..9d78121 100644 --- a/apps/web/app/api/v1/slack/oauth/callback/route.ts +++ b/apps/web/app/api/v1/slack/oauth/callback/route.ts @@ -40,7 +40,9 @@ export async function GET(request: Request) { ), ), }; - const installation = await new SlackGovernanceAdapter().install( + const slack = new SlackGovernanceAdapter(); + await slack.consumeOAuthState(subject, state); + const installation = await slack.install( subject, code, process.env.SLACK_REDIRECT_URI ?? "", diff --git a/packages/agent-harness/src/index.test.ts b/packages/agent-harness/src/index.test.ts index 077c395..ccb0d5b 100644 --- a/packages/agent-harness/src/index.test.ts +++ b/packages/agent-harness/src/index.test.ts @@ -77,6 +77,12 @@ describe("Slack governed harness boundary", () => { expect(failedActions.elements.map((element) => element.action_id)).toContain("muster.retry"); expect(failedActions.elements.map((element) => element.action_id)).toContain("muster.approval.view"); expect(JSON.stringify(failed)).not.toContain("connector-token"); + + const escaped = slackResultBlocks("Jessie", "completed", { + summary: "External <@U123> & ", + }); + expect(JSON.stringify(escaped)).toContain("<@U123>"); + expect(JSON.stringify(escaped)).not.toContain("<@U123>"); }); it("keeps Hermes, MCP, CLI, and HTTP invocations on one portable contract", () => { diff --git a/packages/agent-harness/src/index.ts b/packages/agent-harness/src/index.ts index 0dd9887..7b1c4b7 100644 --- a/packages/agent-harness/src/index.ts +++ b/packages/agent-harness/src/index.ts @@ -368,6 +368,25 @@ export function verifySlackOAuthState(value: string) { export class SlackGovernanceAdapter { constructor(private readonly db = database()) {} + async consumeOAuthState(subject: AuthorisationSubject, state: string) { + requireCapability(subject, "administration.manage"); + const stateHash = createHash("sha256").update(state).digest("hex"); + const [consumed] = await this.db + .insert(schema.idempotencyRecords) + .values({ + organisationId: subject.organisationId, + scope: "slack.oauth.state", + key: stateHash, + requestHash: stateHash, + responseStatus: 204, + responseBody: { consumed: true }, + expiresAt: new Date(Date.now() + 15 * 60_000), + }) + .onConflictDoNothing() + .returning({ key: schema.idempotencyRecords.key }); + if (!consumed) throw new Error("Slack OAuth state has already been used"); + } + async install(subject: AuthorisationSubject, code: string, redirectUri: string) { requireCapability(subject, "administration.manage"); const clientId = process.env.SLACK_CLIENT_ID; @@ -391,36 +410,56 @@ export class SlackGovernanceAdapter { encryptionKey(), ); return this.db.transaction(async (tx) => { - const [installation] = await tx - .insert(schema.slackInstallations) - .values({ - id: newId(), - organisationId: subject.organisationId, - teamId: payload.team.id, - teamName: payload.team.name, - enterpriseId: payload.enterprise?.id, - botUserId: payload.bot_user_id, - scopes, - encryptedBotToken, - installedByActorId: subject.actorId, - status: "active", - }) - .onConflictDoUpdate({ - target: schema.slackInstallations.teamId, - set: { - organisationId: subject.organisationId, - teamName: payload.team.name, - enterpriseId: payload.enterprise?.id, - botUserId: payload.bot_user_id, - scopes, - encryptedBotToken, - installedByActorId: subject.actorId, - status: "active", - revokedAt: null, - updatedAt: new Date(), - }, - }) - .returning(); + const [existing] = await tx + .select() + .from(schema.slackInstallations) + .where(eq(schema.slackInstallations.teamId, payload.team.id)) + .limit(1); + if ( + existing && + existing.organisationId !== subject.organisationId + ) { + throw new Error("Slack workspace is already connected to another organisation"); + } + const [installation] = existing + ? await tx + .update(schema.slackInstallations) + .set({ + teamName: payload.team.name, + enterpriseId: payload.enterprise?.id, + botUserId: payload.bot_user_id, + scopes, + encryptedBotToken, + installedByActorId: subject.actorId, + status: "active", + revokedAt: null, + updatedAt: new Date(), + }) + .where( + and( + eq(schema.slackInstallations.id, existing.id), + eq( + schema.slackInstallations.organisationId, + subject.organisationId, + ), + ), + ) + .returning() + : await tx + .insert(schema.slackInstallations) + .values({ + id: newId(), + organisationId: subject.organisationId, + teamId: payload.team.id, + teamName: payload.team.name, + enterpriseId: payload.enterprise?.id, + botUserId: payload.bot_user_id, + scopes, + encryptedBotToken, + installedByActorId: subject.actorId, + status: "active", + }) + .returning(); if (!installation) throw new Error("Slack installation was not persisted"); await appendAuditEvent(tx, { organisationId: subject.organisationId, @@ -502,10 +541,9 @@ export class SlackGovernanceAdapter { payload: Record; }) { if (!envelope.envelope_id.trim()) throw new Error("Slack envelope is missing"); - return this.recordEvent( - JSON.stringify({ envelope_id: envelope.envelope_id, payload: envelope.payload }), - envelope.payload, - ); + // Envelope ids are transport-attempt ids. Hash only the Slack payload when + // it lacks an event id so reconnect delivery cannot invoke an agent twice. + return this.recordEvent(JSON.stringify(envelope.payload), envelope.payload); } async health(subject: AuthorisationSubject) { @@ -826,6 +864,13 @@ function slackText(value: unknown, max = 2_000) { }); } +function slackMrkdwn(value: unknown, max = 2_000) { + return slackText(value, max) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">"); +} + async function slackApi(token: string, method: string, body: Record) { const response = await fetch(`https://slack.com/api/${method}`, { method: "POST", @@ -847,13 +892,13 @@ export function slackResultBlocks(agentName: string, status: string, output: unk output && typeof output === "object" && !Array.isArray(output) ? (output as Record) : {}; - const summary = slackText(result.summary, 1_500) || "No typed result was produced."; + const summary = slackMrkdwn(result.summary, 1_500) || "No typed result was produced."; const confidence = typeof result.confidence === "number" ? `\n*Confidence:* ${Math.round(result.confidence * 100)}%` : ""; const gaps = Array.isArray(result.gaps) - ? slackText(result.gaps.filter((gap): gap is string => typeof gap === "string").slice(0, 3).join("; "), 700) + ? slackMrkdwn(result.gaps.filter((gap): gap is string => typeof gap === "string").slice(0, 3).join("; "), 700) : ""; const actions: Array<{ type: "button"; diff --git a/packages/agent-harness/src/slack.integration.test.ts b/packages/agent-harness/src/slack.integration.test.ts index 0222a67..5170433 100644 --- a/packages/agent-harness/src/slack.integration.test.ts +++ b/packages/agent-harness/src/slack.integration.test.ts @@ -153,6 +153,27 @@ describeIntegration("synthetic Slack governed-agent delivery", () => { expect(replay.duplicate).toBe(true); expect(first.inboxEvent?.id).toBeTruthy(); + const socketPayload = { + type: "event_callback", + team_id: teamId, + event: { + type: "app_mention", + user: slackUserId, + channel: "C-synthetic", + text: "socket replay remains one invocation", + }, + }; + const socketFirst = await adapter.recordSocketEnvelope({ + envelope_id: `envelope-first-${suffix}`, + payload: socketPayload, + }); + const socketReplay = await adapter.recordSocketEnvelope({ + envelope_id: `envelope-retry-${suffix}`, + payload: socketPayload, + }); + expect(socketFirst.duplicate).toBe(false); + expect(socketReplay.duplicate).toBe(true); + await processSlackInboxEvent(first.inboxEvent!.id); await processSlackInboxEvent(first.inboxEvent!.id); const [run] = await db From 11cc3226c5e565fe8d614b1500cbde69cfd46de4 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Mon, 27 Jul 2026 11:48:49 +1000 Subject: [PATCH 29/47] feat(slack): complete governed admin settings --- apps/web/app/api/v1/slack/settings/route.ts | 15 + apps/web/components/slack-settings-view.tsx | 689 +++++++++++++++++- packages/agent-harness/src/index.ts | 137 +++- .../src/slack.integration.test.ts | 175 ++++- tests/slack-settings.spec.ts | 42 ++ 5 files changed, 992 insertions(+), 66 deletions(-) create mode 100644 apps/web/app/api/v1/slack/settings/route.ts create mode 100644 tests/slack-settings.spec.ts diff --git a/apps/web/app/api/v1/slack/settings/route.ts b/apps/web/app/api/v1/slack/settings/route.ts new file mode 100644 index 0000000..edcc160 --- /dev/null +++ b/apps/web/app/api/v1/slack/settings/route.ts @@ -0,0 +1,15 @@ +import { SlackGovernanceAdapter } from "@muster/agent-harness"; +import { apiSubject, problemResponse, requestTraceId } from "@/lib/api-context"; + +export async function GET(request: Request) { + const traceId = requestTraceId(request); + try { + const subject = await apiSubject(request); + return Response.json({ + data: await new SlackGovernanceAdapter().settings(subject), + traceId, + }); + } catch (error) { + return problemResponse(error, traceId); + } +} diff --git a/apps/web/components/slack-settings-view.tsx b/apps/web/components/slack-settings-view.tsx index 290307e..c8f37ad 100644 --- a/apps/web/components/slack-settings-view.tsx +++ b/apps/web/components/slack-settings-view.tsx @@ -1,57 +1,259 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { AppShell } from "@/components/app-shell"; import { PageHeader } from "@/components/page-header"; import { Button } from "@/components/ui/button"; -type SlackInstallation = { +type Installation = { id: string; teamId: string; teamName: string | null; + scopes: unknown; status: string; + installedAt: string; lastHealthAt: string | null; lastDeliveryAt: string | null; lastError: string | null; }; +type Actor = { id: string; displayName: string }; +type Agent = { id: string; name: string }; +type Identity = { + id: string; + installationId: string; + slackUserId: string; + actorId: string; + actorName: string; + status: string; + createdAt: string; +}; +type Exposure = { + id: string; + installationId: string; + agentId: string; + agentName: string; + enabled: boolean; + isDefault: boolean; + allowedChannelIds: unknown; + allowDirectMessages: boolean; + allowThreadContext: boolean; + updatedAt: string; +}; +type Delivery = { + id: string; + installationId: string; + runId: string; + status: string; + attemptCount: number; + lastError: string | null; + updatedAt: string; +}; +type SlackSettings = { + installations: Installation[]; + actors: Actor[]; + agents: Agent[]; + identities: Identity[]; + exposures: Exposure[]; + deliveries: Delivery[]; +}; + +const date = (value: string | null) => + value ? new Date(value).toLocaleString() : "Not recorded"; + +const channels = (value: unknown) => + Array.isArray(value) + ? value.filter((item): item is string => typeof item === "string") + : []; + export function SlackSettingsView() { - const [installations, setInstallations] = useState([]); + const [settings, setSettings] = useState(null); const [error, setError] = useState(null); + const [notice, setNotice] = useState(null); const [loading, setLoading] = useState(true); + const [busy, setBusy] = useState(null); + const [revokeCandidate, setRevokeCandidate] = useState( + null, + ); + const load = useCallback(async () => { setLoading(true); try { - const response = await fetch("/api/v1/slack/health", { cache: "no-store" }); - const payload = (await response.json()) as { data?: SlackInstallation[]; error?: string }; - if (!response.ok) throw new Error(payload.error ?? "Slack health check failed"); - setInstallations(payload.data ?? []); + const response = await fetch("/api/v1/slack/settings", { + cache: "no-store", + }); + const payload = (await response.json()) as { + data?: SlackSettings; + detail?: string; + }; + if (!response.ok || !payload.data) + throw new Error( + payload.detail ?? "Could not load Slack administration settings.", + ); + setSettings(payload.data); setError(null); } catch (cause) { - setError(cause instanceof Error ? cause.message : "Slack health check failed"); + setError( + cause instanceof Error + ? cause.message + : "Could not load Slack administration settings.", + ); } finally { setLoading(false); } }, []); + useEffect(() => void load(), [load]); + const activeInstallations = useMemo( + () => + settings?.installations.filter( + (installation) => installation.status === "active", + ) ?? [], + [settings], + ); + const reconnect = async () => { - const response = await fetch("/api/v1/slack/install", { cache: "no-store" }); - const payload = (await response.json()) as { data?: { authorizationUrl?: string }; error?: string }; - if (!response.ok || !payload.data?.authorizationUrl) { - setError(payload.error ?? "Could not start Slack OAuth"); - return; + setBusy("reconnect"); + setNotice(null); + try { + const response = await fetch("/api/v1/slack/install", { + cache: "no-store", + }); + const payload = (await response.json()) as { + data?: { authorizationUrl?: string }; + detail?: string; + }; + if (!response.ok || !payload.data?.authorizationUrl) + throw new Error(payload.detail ?? "Could not start Slack OAuth."); + window.location.assign(payload.data.authorizationUrl); + } catch (cause) { + setError( + cause instanceof Error ? cause.message : "Could not start Slack OAuth.", + ); + } finally { + setBusy(null); + } + }; + + const refreshHealth = async () => { + setBusy("health"); + setNotice(null); + try { + const response = await fetch("/api/v1/slack/health", { + cache: "no-store", + }); + const payload = (await response.json()) as { detail?: string }; + if (!response.ok) + throw new Error(payload.detail ?? "Slack health refresh failed."); + await load(); + setNotice("Slack diagnostics refreshed."); + } catch (cause) { + setError( + cause instanceof Error ? cause.message : "Slack health refresh failed.", + ); + } finally { + setBusy(null); + } + }; + + const request = async ( + url: string, + method: "POST" | "PUT", + body: unknown, + ) => { + const response = await fetch(url, { + method, + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + const payload = (await response.json()) as { detail?: string }; + if (!response.ok) + throw new Error(payload.detail ?? "Slack administration update failed."); + }; + + const saveIdentity = async (form: HTMLFormElement) => { + const values = new FormData(form); + setBusy("identity"); + setNotice(null); + try { + await request("/api/v1/slack/identities", "POST", { + installationId: values.get("installationId"), + slackUserId: values.get("slackUserId"), + actorId: values.get("actorId"), + }); + form.reset(); + await load(); + setNotice("Slack user mapping saved."); + } catch (cause) { + setError( + cause instanceof Error + ? cause.message + : "Could not save Slack user mapping.", + ); + } finally { + setBusy(null); } - window.location.assign(payload.data.authorizationUrl); }; - const revoke = async (installationId: string) => { - const response = await fetch( - `/api/v1/slack/install?installationId=${encodeURIComponent(installationId)}`, - { method: "DELETE" }, - ); - if (!response.ok) setError("Could not revoke Slack installation"); - await load(); + const saveExposure = async (form: HTMLFormElement) => { + const values = new FormData(form); + const allowedChannelIds = String(values.get("allowedChannelIds") ?? "") + .split(/[\n,]/) + .map((value) => value.trim()) + .filter(Boolean); + const enabled = values.get("enabled") === "on"; + setBusy("exposure"); + setNotice(null); + try { + await request("/api/v1/slack/exposures", "PUT", { + installationId: values.get("installationId"), + agentId: values.get("agentId"), + enabled, + isDefault: enabled && values.get("isDefault") === "on", + allowedChannelIds, + allowDirectMessages: values.get("allowDirectMessages") === "on", + allowThreadContext: values.get("allowThreadContext") === "on", + }); + await load(); + setNotice("Agent exposure policy saved."); + } catch (cause) { + setError( + cause instanceof Error + ? cause.message + : "Could not save agent exposure policy.", + ); + } finally { + setBusy(null); + } + }; + + const revoke = async () => { + if (!revokeCandidate) return; + setBusy("revoke"); + setNotice(null); + try { + const response = await fetch( + `/api/v1/slack/install?installationId=${encodeURIComponent(revokeCandidate.id)}`, + { method: "DELETE" }, + ); + const payload = (await response.json()) as { detail?: string }; + if (!response.ok) + throw new Error( + payload.detail ?? "Could not revoke Slack installation.", + ); + setRevokeCandidate(null); + await load(); + setNotice("Slack installation revoked. Existing tokens were replaced."); + } catch (cause) { + setError( + cause instanceof Error + ? cause.message + : "Could not revoke Slack installation.", + ); + } finally { + setBusy(null); + } }; return ( @@ -59,30 +261,439 @@ export function SlackSettingsView() { void reconnect()}>Connect or reconnect Slack} + description="Organisation-scoped Slack identities, agent exposure, delivery health, and reconnect controls." + actions={ +
+ + +
+ } /> -
- {error ?

{error}

: null} - {loading ?

Checking Slack health…

: null} - {!loading && installations.length === 0 ? ( -

No Slack workspace is connected.

+ +
+ {error ? ( +

+ {error} +

+ ) : null} + {notice ? ( +

+ {notice} +

+ ) : null} + {loading ? ( +

+ Loading Slack administration… +

) : null} -
- {installations.map((installation) => ( -
-
-

{installation.teamName ?? installation.teamId}

-

- {installation.status} · health {installation.lastHealthAt ?? "not checked"} · delivery {installation.lastDeliveryAt ?? "none"} + + {!loading && settings ? ( + <> +

+
+
+

+ Workspace connections +

+

+ Tokens stay encrypted; this view only shows redacted + operator diagnostics. +

+
+
+ {settings.installations.length === 0 ? ( +

+ No Slack workspace is connected. Connect Slack to begin a + governed installation.

- {installation.lastError ?

{installation.lastError}

: null} + ) : null} +
+ {settings.installations.map((installation) => ( +
+
+

+ {installation.teamName ?? installation.teamId} +

+

+ {installation.status} · installed{" "} + {date(installation.installedAt)} +

+

+ Health: {date(installation.lastHealthAt)} · latest + delivery: {date(installation.lastDeliveryAt)} +

+

+ Scopes:{" "} + {channels(installation.scopes).join(", ") || + "Not recorded"} +

+ {installation.lastError ? ( +

+ {installation.lastError} +

+ ) : null} +
+ {installation.status === "active" ? ( + + ) : null} +
+ ))}
- +
+ +
+
{ + event.preventDefault(); + void saveIdentity(event.currentTarget); + }} + > +

+ Map a Slack user +

+

+ Map one Slack user to one active Muster human. Approvals still + require the authoritative Muster capability checks. +

+ + + + +
+ {settings.identities.length === 0 ? ( +

+ No Slack identities are mapped yet. +

+ ) : ( + settings.identities.map((identity) => ( +

+ + {identity.slackUserId} + {" "} + → {identity.actorName} · {identity.status} +

+ )) + )} +
+
+ +
{ + event.preventDefault(); + void saveExposure(event.currentTarget); + }} + > +

+ Agent exposure policy +

+

+ Channel mentions are allowed only for listed channel IDs. + Direct-message and thread-context access are explicit per + agent. +

+ + +