diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a4f1c83b77..8b1bff3b121 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -937,6 +937,10 @@ jobs: if: matrix.part == 'unit' run: pnpm --filter cloud-agent-next test + - name: Build runtime artifacts for Worker integration tests + if: matrix.part == 'workers' + run: pnpm --filter cloud-agent-next build:wrapper + - name: Run cloud-agent-next Workers tests if: matrix.part == 'workers' run: pnpm --filter cloud-agent-next test:integration diff --git a/.github/workflows/deploy-workers.yml b/.github/workflows/deploy-workers.yml index f284154eb06..a94827aadd9 100644 --- a/.github/workflows/deploy-workers.yml +++ b/.github/workflows/deploy-workers.yml @@ -67,6 +67,12 @@ jobs: node-version-file: '.nvmrc' cache: 'pnpm' + - name: Setup Bun + if: inputs.worker == 'services/cloud-agent-next' + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: 1.4.4 + - name: Install dependencies working-directory: ${{ inputs.worker }} run: pnpm install --frozen-lockfile @@ -338,6 +344,12 @@ jobs: node-version-file: '.nvmrc' cache: 'pnpm' + - name: Setup Bun + if: matrix.worker == 'services/cloud-agent-next' + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: 1.4.4 + - name: Install dependencies working-directory: ${{ matrix.worker }} run: pnpm install --frozen-lockfile diff --git a/.kilo/skills/kilo-mcp/SKILL.md b/.kilo/skills/kilo-mcp/SKILL.md index 42cf77e78e9..b7ad6dbad14 100644 --- a/.kilo/skills/kilo-mcp/SKILL.md +++ b/.kilo/skills/kilo-mcp/SKILL.md @@ -98,5 +98,6 @@ exist as an org-scoped mirror under `organizations`, for example - `user` - `userExports` - `userFeedback` +- `vercelCompute` - `webhookTriggers` - `workspaceFolders` diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md index a2bcabad05d..3a9f07c1d5a 100644 --- a/ENVIRONMENT.md +++ b/ENVIRONMENT.md @@ -471,10 +471,10 @@ The key is team-scoped for all topics and valid in both the sandbox and producti - `WORKSPACE_PATH` - Filesystem path of the agent workspace. [SERVER] - `SESSION_ID` - Reserved session identifier for the `cloud-agent-next` runtime; reserved in `RESERVED_ENV_VARS`. [SERVER] - `SANDBOX_SELECTION_IDS` - Comma-separated user or org IDs allowed to pick a Cloud Agent sandbox destination on the new-session page. Empty admits nobody. `*` includes personal accounts. Production `wrangler.jsonc` sets the allowlist, so a deploy overwrites the Cloudflare dashboard value. Wrangler `dev` and `.dev.vars.example` default to `*`. [SERVER] -- `VERCEL_SANDBOX_ORG_IDS` - Comma-separated org IDs routed to Vercel sandboxes. Empty is off. `*` includes personal accounts. [SERVER] - `CLOUDFLARE_CONTAINERS_ORG_IDS` - Comma-separated org IDs enrolled for the DO-managed Cloudflare Containers runtime, which is also the default sandbox destination for isolated control-plane sessions. Empty is off. `*` includes every organization and personal account. Production `wrangler.jsonc` sets `*` so a deploy overwrites the Cloudflare dashboard value; wrangler `dev` also sets `*`. [SERVER] - `PREVIEW_HOST_SUFFIX` - Hostname suffix the Cloud Agent Next Worker dispatches preview hosts under; a preview host is `p-.`. Unset or empty disables preview dispatch and the `/preview/open` hop, and the web app hides the preview button (it reads availability from the Worker's `getPreviewOptions`). The web app needs no preview setting. Production `wrangler.jsonc` sets `cloud-agent.kiloapps.ai`. `services/cloud-agent-next/.dev.vars.example` sets `localhost`. [SERVER] - `PREVIEW_ENABLED_IDS` - Comma-separated user or org IDs that the Cloud Agent Next Worker shows the preview button and preview links to. `*` includes personal accounts. Unset or empty shows nobody. A UI gate only: `getPreviewOptions` answers `enabled: false` for other owners, but the `/preview/open` hop and the preview host do not check it. The org ID comes from the web route and is not verified. Production `wrangler.jsonc` sets `*`, so a deploy overwrites the Cloudflare dashboard value. Wrangler `dev` and `.dev.vars.example` default to `*`. [SERVER] +- `BYOC_VERCEL_IDS` - Comma-separated user or organization IDs enrolled in customer-paid Vercel compute. Empty is off; `*` enrolls both personal accounts and organizations. An enrolled owner fails closed when its ready credential is absent. [SERVER] - `HOME` - Reserved in `RESERVED_ENV_VARS` for cloud-agent-next session home management. [SYSTEM] ### Gastown diff --git a/apps/web/src/app/(app)/organizations/[id]/cloud/compute/page.tsx b/apps/web/src/app/(app)/organizations/[id]/cloud/compute/page.tsx new file mode 100644 index 00000000000..fb2a3718045 --- /dev/null +++ b/apps/web/src/app/(app)/organizations/[id]/cloud/compute/page.tsx @@ -0,0 +1,18 @@ +import { notFound, redirect } from 'next/navigation'; +import { OrganizationByPageLayout } from '@/components/organizations/OrganizationByPageLayout'; + +export default async function OrganizationComputePage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + return ( + { + if (!isGlobalAdmin && role !== 'owner' && role !== 'admin') notFound(); + redirect(`/organizations/${organization.id}/cloud?settings=compute`); + }} + /> + ); +} diff --git a/apps/web/src/app/api/internal/byoc/vercel-credentials/[credentialId]/route.ts b/apps/web/src/app/api/internal/byoc/vercel-credentials/[credentialId]/route.ts new file mode 100644 index 00000000000..bd2441d05b1 --- /dev/null +++ b/apps/web/src/app/api/internal/byoc/vercel-credentials/[credentialId]/route.ts @@ -0,0 +1,269 @@ +import { timingSafeEqual } from '@kilocode/encryption'; +import { and, eq, type SQL } from 'drizzle-orm'; +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; +import { z } from 'zod'; + +import { + organization_vercel_compute_credentials, + type OrganizationVercelComputeCredential, +} from '@kilocode/db/schema'; +import { db } from '@kilocode/web-shared/lib/drizzle'; +import { INTERNAL_API_SECRET } from '@kilocode/web-shared/lib/config.server'; +import { applyVercelComputeStatusProjection } from '@/lib/cloud-agent-next/vercel-compute-status'; + +const SetupStatusSchema = z.enum(['pending', 'building', 'ready', 'failed']); +const SetupStepSchema = z + .enum([ + 'validating_access', + 'create_builder', + 'install_system_dependencies', + 'install_node_dependencies', + 'upload_runtime_artifacts', + 'verify_runtime_artifacts', + 'snapshot_builder', + 'create_validator', + 'launch_validator_wrapper', + 'verify_validator_call_home', + 'stop_validator', + 'confirm_terminal', + ]) + .nullable(); + +const CredentialOwnerSchema = z.union([ + z.object({ organizationId: z.uuid(), userId: z.never().optional() }), + z.object({ organizationId: z.never().optional(), userId: z.string().min(1) }), +]); + +const CredentialUpdateSchema = z + .object({ + buildGeneration: z.uuid(), + setupStatus: SetupStatusSchema, + setupStep: SetupStepSchema, + setupError: z + .string() + .regex(/^[a-z0-9_]+$/) + .max(96) + .nullable(), + teamSlug: z.string().min(1).max(256).nullable(), + projectSlug: z.string().min(1).max(256).nullable(), + runtimeBuildId: z.string().min(1).max(256).nullable(), + runtimeSnapshotId: z.string().min(1).max(256).nullable(), + setupStartedAt: z.iso.datetime().nullable(), + setupCompletedAt: z.iso.datetime().nullable(), + runtimeWrapperVersion: z.string().min(1).max(64).nullable().optional(), + runtimeReleasedAt: z + .string() + .regex(/^\d{4}-\d{2}-\d{2}$/) + .nullable() + .optional(), + runtimeDigest: z.string().min(1).max(512).nullable().optional(), + }) + .and(CredentialOwnerSchema) + .superRefine((value, ctx) => { + if (value.setupStatus !== 'ready') return; + const required: Array = [ + 'projectSlug', + 'runtimeSnapshotId', + 'setupCompletedAt', + ]; + for (const field of required) { + if (!value[field]) { + ctx.addIssue({ + code: 'custom', + path: [field], + message: 'Ready status requires the completed runtime fields', + }); + } + } + }); + +function responseHeaders(): HeadersInit { + return { 'Cache-Control': 'no-store' }; +} + +function unauthorized(): NextResponse { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401, headers: responseHeaders() }); +} + +function isAuthorized(request: NextRequest): boolean { + const presented = request.headers.get('x-internal-api-key'); + return Boolean( + INTERNAL_API_SECRET && presented && timingSafeEqual(presented, INTERNAL_API_SECRET) + ); +} + +function toCredentialResponse(row: OrganizationVercelComputeCredential) { + const response = { + credentialId: row.id, + tokenEncrypted: row.token_encrypted, + tokenScope: row.token_scope, + teamId: row.team_id, + projectId: row.project_id, + teamSlug: row.team_slug, + projectSlug: row.project_slug, + setupStatus: row.setup_status, + setupStep: row.setup_step, + setupError: row.setup_error, + buildGeneration: row.build_generation, + runtimeBuildId: row.runtime_build_id, + runtimeSnapshotId: row.runtime_snapshot_id, + runtimeWrapperVersion: row.runtime_wrapper_version, + runtimeReleasedAt: row.runtime_released_at, + runtimeDigest: row.runtime_digest, + upgradeStatus: row.upgrade_status, + upgradeStep: row.upgrade_step, + upgradeError: row.upgrade_error, + setupStartedAt: row.setup_started_at ? new Date(row.setup_started_at).toISOString() : null, + setupCompletedAt: row.setup_completed_at + ? new Date(row.setup_completed_at).toISOString() + : null, + }; + + if (row.organization_id !== null && row.user_id === null) { + return { ...response, organizationId: row.organization_id }; + } + if (row.organization_id === null && row.user_id !== null) { + return { ...response, userId: row.user_id }; + } + throw new Error('Vercel compute credential has invalid ownership'); +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ credentialId: string }> } +): Promise { + if (!isAuthorized(request)) return unauthorized(); + + const { credentialId } = await params; + if (!z.uuid().safeParse(credentialId).success) { + return NextResponse.json( + { error: 'Invalid request' }, + { status: 400, headers: responseHeaders() } + ); + } + + const organizationIdParam = request.nextUrl.searchParams.get('organizationId'); + const userIdParam = request.nextUrl.searchParams.get('userId'); + if ((organizationIdParam === null) === (userIdParam === null)) { + return NextResponse.json( + { error: 'Invalid request' }, + { status: 400, headers: responseHeaders() } + ); + } + + let ownerFilter: SQL; + if (organizationIdParam !== null) { + const organizationIdResult = z.uuid().safeParse(organizationIdParam); + if (!organizationIdResult.success) { + return NextResponse.json( + { error: 'Invalid request' }, + { status: 400, headers: responseHeaders() } + ); + } + ownerFilter = eq( + organization_vercel_compute_credentials.organization_id, + organizationIdResult.data + ); + } else { + const userIdResult = z.string().min(1).safeParse(userIdParam); + if (!userIdResult.success) { + return NextResponse.json( + { error: 'Invalid request' }, + { status: 400, headers: responseHeaders() } + ); + } + ownerFilter = eq(organization_vercel_compute_credentials.user_id, userIdResult.data); + } + + const row = await db.query.organization_vercel_compute_credentials.findFirst({ + where: and(eq(organization_vercel_compute_credentials.id, credentialId), ownerFilter), + }); + if (!row) { + return NextResponse.json( + { error: 'Credential not found' }, + { status: 404, headers: responseHeaders() } + ); + } + + return NextResponse.json(toCredentialResponse(row), { headers: responseHeaders() }); +} + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ credentialId: string }> } +): Promise { + if (!isAuthorized(request)) return unauthorized(); + + const { credentialId } = await params; + if (!z.uuid().safeParse(credentialId).success) { + return NextResponse.json( + { error: 'Invalid request' }, + { status: 400, headers: responseHeaders() } + ); + } + + const parsed = CredentialUpdateSchema.safeParse(await request.json().catch(() => null)); + if (!parsed.success) { + return NextResponse.json( + { error: 'Invalid status update' }, + { status: 400, headers: responseHeaders() } + ); + } + + const { data } = parsed; + let ownerFilter: SQL; + if (data.organizationId !== undefined) { + ownerFilter = eq(organization_vercel_compute_credentials.organization_id, data.organizationId); + } else if (data.userId !== undefined) { + ownerFilter = eq(organization_vercel_compute_credentials.user_id, data.userId); + } else { + return NextResponse.json( + { error: 'Invalid status update' }, + { status: 400, headers: responseHeaders() } + ); + } + + const current = await db.query.organization_vercel_compute_credentials.findFirst({ + where: and( + eq(organization_vercel_compute_credentials.id, credentialId), + ownerFilter, + eq(organization_vercel_compute_credentials.build_generation, data.buildGeneration) + ), + }); + if (!current) { + return NextResponse.json({ updated: false }, { headers: responseHeaders() }); + } + + const next = applyVercelComputeStatusProjection(current, { + setupStatus: data.setupStatus, + setupStep: data.setupStep, + setupError: data.setupError, + teamSlug: data.teamSlug, + projectSlug: data.projectSlug, + runtimeBuildId: data.runtimeBuildId, + runtimeSnapshotId: data.runtimeSnapshotId, + setupStartedAt: data.setupStartedAt, + setupCompletedAt: data.setupCompletedAt, + runtimeWrapperVersion: data.runtimeWrapperVersion ?? null, + runtimeReleasedAt: data.runtimeReleasedAt ?? null, + runtimeDigest: data.runtimeDigest ?? null, + }); + + const [updated] = await db + .update(organization_vercel_compute_credentials) + .set({ + ...next, + updated_at: new Date().toISOString(), + }) + .where( + and( + eq(organization_vercel_compute_credentials.id, credentialId), + ownerFilter, + eq(organization_vercel_compute_credentials.build_generation, data.buildGeneration) + ) + ) + .returning({ id: organization_vercel_compute_credentials.id }); + + return NextResponse.json({ updated: Boolean(updated) }, { headers: responseHeaders() }); +} diff --git a/apps/web/src/app/api/internal/byoc/vercel-credentials/organization/[organizationId]/route.ts b/apps/web/src/app/api/internal/byoc/vercel-credentials/organization/[organizationId]/route.ts new file mode 100644 index 00000000000..0e4ed5e4f0e --- /dev/null +++ b/apps/web/src/app/api/internal/byoc/vercel-credentials/organization/[organizationId]/route.ts @@ -0,0 +1,73 @@ +import { timingSafeEqual } from '@kilocode/encryption'; +import { eq } from 'drizzle-orm'; +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; +import { z } from 'zod'; + +import { organization_vercel_compute_credentials } from '@kilocode/db/schema'; +import { db } from '@kilocode/web-shared/lib/drizzle'; +import { INTERNAL_API_SECRET } from '@kilocode/web-shared/lib/config.server'; + +const responseHeaders = { 'Cache-Control': 'no-store' }; + +function isAuthorized(request: NextRequest): boolean { + const presented = request.headers.get('x-internal-api-key'); + return Boolean( + INTERNAL_API_SECRET && presented && timingSafeEqual(presented, INTERNAL_API_SECRET) + ); +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ organizationId: string }> } +): Promise { + if (!isAuthorized(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401, headers: responseHeaders }); + } + + const { organizationId } = await params; + if (!z.uuid().safeParse(organizationId).success) { + return NextResponse.json( + { error: 'Invalid request' }, + { status: 400, headers: responseHeaders } + ); + } + + const row = await db.query.organization_vercel_compute_credentials.findFirst({ + where: eq(organization_vercel_compute_credentials.organization_id, organizationId), + columns: { + id: true, + organization_id: true, + setup_status: true, + setup_step: true, + setup_error: true, + build_generation: true, + runtime_build_id: true, + runtime_snapshot_id: true, + team_slug: true, + project_slug: true, + }, + }); + if (!row) { + return NextResponse.json( + { error: 'Credential not found' }, + { status: 404, headers: responseHeaders } + ); + } + + return NextResponse.json( + { + credentialId: row.id, + organizationId: row.organization_id, + setupStatus: row.setup_status, + setupStep: row.setup_step, + setupError: row.setup_error, + buildGeneration: row.build_generation, + runtimeBuildId: row.runtime_build_id, + runtimeSnapshotId: row.runtime_snapshot_id, + teamSlug: row.team_slug, + projectSlug: row.project_slug, + }, + { headers: responseHeaders } + ); +} diff --git a/apps/web/src/app/api/internal/byoc/vercel-credentials/user/[userId]/route.ts b/apps/web/src/app/api/internal/byoc/vercel-credentials/user/[userId]/route.ts new file mode 100644 index 00000000000..3ad8174ef86 --- /dev/null +++ b/apps/web/src/app/api/internal/byoc/vercel-credentials/user/[userId]/route.ts @@ -0,0 +1,73 @@ +import { timingSafeEqual } from '@kilocode/encryption'; +import { eq } from 'drizzle-orm'; +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; +import { z } from 'zod'; + +import { organization_vercel_compute_credentials } from '@kilocode/db/schema'; +import { db } from '@kilocode/web-shared/lib/drizzle'; +import { INTERNAL_API_SECRET } from '@kilocode/web-shared/lib/config.server'; + +const responseHeaders = { 'Cache-Control': 'no-store' }; + +function isAuthorized(request: NextRequest): boolean { + const presented = request.headers.get('x-internal-api-key'); + return Boolean( + INTERNAL_API_SECRET && presented && timingSafeEqual(presented, INTERNAL_API_SECRET) + ); +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ userId: string }> } +): Promise { + if (!isAuthorized(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401, headers: responseHeaders }); + } + + const { userId } = await params; + if (!z.string().min(1).safeParse(userId).success) { + return NextResponse.json( + { error: 'Invalid request' }, + { status: 400, headers: responseHeaders } + ); + } + + const row = await db.query.organization_vercel_compute_credentials.findFirst({ + where: eq(organization_vercel_compute_credentials.user_id, userId), + columns: { + id: true, + user_id: true, + setup_status: true, + setup_step: true, + setup_error: true, + build_generation: true, + runtime_build_id: true, + runtime_snapshot_id: true, + team_slug: true, + project_slug: true, + }, + }); + if (!row) { + return NextResponse.json( + { error: 'Credential not found' }, + { status: 404, headers: responseHeaders } + ); + } + + return NextResponse.json( + { + credentialId: row.id, + userId: row.user_id, + setupStatus: row.setup_status, + setupStep: row.setup_step, + setupError: row.setup_error, + buildGeneration: row.build_generation, + runtimeBuildId: row.runtime_build_id, + runtimeSnapshotId: row.runtime_snapshot_id, + teamSlug: row.team_slug, + projectSlug: row.project_slug, + }, + { headers: responseHeaders } + ); +} diff --git a/apps/web/src/components/cloud-agent-next/sandbox-selection.test.ts b/apps/web/src/components/cloud-agent-next/sandbox-selection.test.ts index 6893b43eacd..6d5c7671a1e 100644 --- a/apps/web/src/components/cloud-agent-next/sandbox-selection.test.ts +++ b/apps/web/src/components/cloud-agent-next/sandbox-selection.test.ts @@ -30,7 +30,7 @@ describe('formatSandboxCapacity', () => { [getSandboxAllocationRequest('isolated-standard'), '4 vCPU / 12 GiB'], [getSandboxAllocationRequest('vercel-small'), '2 vCPU / 4 GiB'], [byocLarge, '4 vCPU / 8 GiB'], - [{ provider: { id: 'vercel', account: 'kilo' }, instanceType: 'default' }, 'Provider default'], + [{ provider: { id: 'vercel', account: 'byoc' }, instanceType: 'default' }, 'Provider default'], ] satisfies Array<[SandboxDestination, string]>)( 'reports the vCPU and memory a destination runs with: %j', (destination, label) => { @@ -47,7 +47,7 @@ describe('formatSandboxInstance', () => { [getSandboxAllocationRequest('cloudflare-containers-standard-4'), '4 vCPU / 12 GiB · Large'], [getSandboxAllocationRequest('vercel-small'), '2 vCPU / 4 GiB · Small'], [byocLarge, '4 vCPU / 8 GiB · Medium'], - [{ provider: { id: 'vercel', account: 'kilo' }, instanceType: 'default' }, 'Provider default'], + [{ provider: { id: 'vercel', account: 'byoc' }, instanceType: 'default' }, 'Provider default'], ] satisfies Array<[SandboxDestination, string]>)( 'formats an instance without repeating its account/provider heading: %j', (destination, label) => { @@ -75,17 +75,17 @@ describe('getSandboxSelectionOptions', () => { ]; const originalOrder = [...options]; expect(getSandboxSelectionOptions({ enabled: true, options })).toEqual([ + options[0], options[1], options[3], + options[4], options[2], options[5], - options[0], - options[4], ]); expect(options).toEqual(originalOrder); }); - it('does not invent BYOC options when the Worker only offers Kilo', () => { + it('uses only the options offered by the Worker', () => { expect(getSandboxSelectionOptions(capabilities)).toEqual(capabilities.options); expect(getSandboxSelectionOptions(undefined)).toEqual([]); expect(getSandboxSelectionOptions({ enabled: false, options: [] })).toEqual([]); diff --git a/apps/web/src/components/cloud-agent-next/sandbox-selection.ts b/apps/web/src/components/cloud-agent-next/sandbox-selection.ts index 20ae4252ef8..c013b1e7825 100644 --- a/apps/web/src/components/cloud-agent-next/sandbox-selection.ts +++ b/apps/web/src/components/cloud-agent-next/sandbox-selection.ts @@ -1,5 +1,5 @@ import { - getKiloSandboxAllocation, + getSandboxAllocation, getSandboxAllocationResources, type SandboxDestination, type SandboxSelectionCapabilities, @@ -46,13 +46,11 @@ export function formatSandboxProviderName(provider: SandboxDestination['provider } /** - * Formats the fixed Vercel instance resources. Both accounts run the same - * hardware, so the Kilo allocation is the existing mapping to the single - * resource table in worker-utils. + * Formats the Vercel BYOC resources from the shared allocation mapping. */ function formatVercelCapacity(instanceType: 'small' | 'large'): string | null { const resources = getSandboxAllocationResources( - getKiloSandboxAllocation({ provider: { id: 'vercel', account: 'kilo' }, instanceType }) + getSandboxAllocation({ provider: { id: 'vercel', account: 'byoc' }, instanceType }) ); return resources ? formatContainerCapacity({ diff --git a/apps/web/src/components/cloud-agent-next/settings/CloudAgentSettingsButton.tsx b/apps/web/src/components/cloud-agent-next/settings/CloudAgentSettingsButton.tsx index 7cfea43cd89..c6f2968aff4 100644 --- a/apps/web/src/components/cloud-agent-next/settings/CloudAgentSettingsButton.tsx +++ b/apps/web/src/components/cloud-agent-next/settings/CloudAgentSettingsButton.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import { useSearchParams } from 'next/navigation'; import { Settings } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; @@ -8,7 +9,9 @@ import { CloudAgentSettingsDialog } from './CloudAgentSettingsDialog'; /** Topbar button that opens the Cloud Agent settings dialog. */ export function CloudAgentSettingsButton({ organizationId }: { organizationId?: string }) { - const [open, setOpen] = useState(false); + const searchParams = useSearchParams(); + const openCompute = organizationId !== undefined && searchParams.get('settings') === 'compute'; + const [open, setOpen] = useState(openCompute); return ( <> @@ -28,6 +31,7 @@ export function CloudAgentSettingsButton({ organizationId }: { organizationId?: diff --git a/apps/web/src/components/cloud-agent-next/settings/CloudAgentSettingsDialog.tsx b/apps/web/src/components/cloud-agent-next/settings/CloudAgentSettingsDialog.tsx index efda30e0162..e3b50a85bd6 100644 --- a/apps/web/src/components/cloud-agent-next/settings/CloudAgentSettingsDialog.tsx +++ b/apps/web/src/components/cloud-agent-next/settings/CloudAgentSettingsDialog.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { Boxes, FolderCog, Lock, SlidersHorizontal } from 'lucide-react'; +import { Boxes, Cpu, FolderCog, Lock, SlidersHorizontal } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { @@ -19,12 +19,13 @@ import { useIsMobile } from '@/hooks/use-mobile'; import { DefaultSandboxSection } from './DefaultSandboxSection'; import { EnvironmentsSection } from './EnvironmentsSection'; import { RepositoryOverridesSection } from './RepositoryOverridesSection'; +import { VercelComputeSettings } from './VercelComputeSettings'; type CloudAgentSettingsDialogProps = { organizationId?: string; open: boolean; onOpenChange: (open: boolean) => void; - initialSection?: 'general' | 'profiles' | 'environments'; + initialSection?: 'general' | 'profiles' | 'environments' | 'compute'; onProfileSelect?: (profileId: string) => void; openToNewProfile?: boolean; initialSelectedProfileId?: string | null; @@ -69,6 +70,8 @@ function CloudAgentSettings({ const settingsQuery = useCloudAgentSettings({ organizationId }); const settings = settingsQuery.data; const canManage = organizationId ? (settings?.canManage ?? false) : true; + const activeSection = + section === 'compute' && settings && !settings.canManage ? 'general' : section; return ( <> @@ -77,7 +80,7 @@ function CloudAgentSettings({ Cloud Agent settings {organizationId ? 'Organization' : 'Personal'} - {section !== 'profiles' && settings && !settings.canManage ? ( + {settings && !settings.canManage ? ( View only. Organization owners and admins can change these settings. @@ -90,11 +93,15 @@ function CloudAgentSettings({ { - if (value === 'general' || value === 'profiles' || value === 'environments') { + if ( + value === 'general' || + value === 'profiles' || + value === 'environments' || + (value === 'compute' && settings?.canManage) + ) setSection(value); - } }} orientation={isMobile ? 'horizontal' : 'vertical'} className="flex min-h-0 flex-1 flex-col md:flex-row" @@ -124,6 +131,15 @@ function CloudAgentSettings({ Profiles + {settings?.canManage && ( + + + Compute + + )} + + {settings?.canManage ? ( + + ) : settingsQuery.isError ? ( + void settingsQuery.refetch()} /> + ) : ( + + )} + ); diff --git a/apps/web/src/components/cloud-agent-next/settings/VercelComputeSettings.tsx b/apps/web/src/components/cloud-agent-next/settings/VercelComputeSettings.tsx new file mode 100644 index 00000000000..f1c02e05162 --- /dev/null +++ b/apps/web/src/components/cloud-agent-next/settings/VercelComputeSettings.tsx @@ -0,0 +1,933 @@ +'use client'; + +import Link from 'next/link'; +import { useVercelComputeData, type VercelComputeRemoveError } from '@/hooks/useVercelComputeData'; +import { useLayoutEffect, useRef, useState, type FormEvent } from 'react'; +import { AlertTriangle, Check, Circle, ExternalLink, Loader2, MoreHorizontal } from 'lucide-react'; +import { toast } from 'sonner'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +const SETUP_STEPS = [ + ['validating_access', 'Validate Vercel access'], + ['create_builder', 'Create a temporary builder'], + ['install_system_dependencies', 'Install system dependencies'], + ['install_node_dependencies', 'Install runtime dependencies'], + ['upload_runtime_artifacts', 'Upload Kilo runtime'], + ['verify_runtime_artifacts', 'Verify runtime artifacts'], + ['snapshot_builder', 'Create runtime snapshot'], + ['create_validator', 'Create a validation sandbox'], + ['launch_validator_wrapper', 'Launch call-home validation'], + ['verify_validator_call_home', 'Verify call-home connection'], + ['stop_validator', 'Stop validation sandbox'], + ['confirm_terminal', 'Confirm cleanup'], +] as const; + +function dashboardUrl(teamSlug: string | null, projectSlug: string | null): string | null { + if (!teamSlug || !projectSlug) return null; + return `https://vercel.com/${encodeURIComponent(teamSlug)}/${encodeURIComponent(projectSlug)}`; +} + +function projectLabel( + teamSlug: string | null, + projectSlug: string | null, + projectId: string +): string { + if (teamSlug && projectSlug) return `${teamSlug}/${projectSlug}`; + return projectSlug || projectId; +} + +function formatRuntimeLabel(version: string | null, releasedAt: string | null): string { + if (!version && !releasedAt) return 'Unknown'; + const date = releasedAt + ? new Date(`${releasedAt}T00:00:00.000Z`).toLocaleDateString('en-GB', { + day: 'numeric', + month: 'short', + year: 'numeric', + timeZone: 'UTC', + }) + : null; + if (version && date) return `${version} · ${date}`; + return version ?? date ?? 'Unknown'; +} + +function statusCopy( + status: string, + enrollment: 'enrolled' | 'not-enrolled' | 'unavailable' | 'checking' +): { + label: string; + variant: 'default' | 'secondary' | 'destructive'; +} { + if (status === 'ready' && enrollment !== 'enrolled') { + return { + label: + enrollment === 'checking' + ? 'Checking enrollment' + : enrollment === 'unavailable' + ? 'Unavailable' + : 'Not enrolled', + variant: 'secondary', + }; + } + + switch (status) { + case 'ready': + return { label: 'Ready', variant: 'default' }; + case 'failed': + return { label: 'Setup failed', variant: 'destructive' }; + case 'building': + return { label: 'Setting up', variant: 'secondary' }; + default: + return { label: 'Not started', variant: 'secondary' }; + } +} + +export function VercelComputeSettings({ organizationId }: { organizationId?: string }) { + const [token, setToken] = useState(''); + const [teamId, setTeamId] = useState(''); + const [projectId, setProjectId] = useState(''); + const [setupError, setSetupError] = useState(null); + const [removeOpen, setRemoveOpen] = useState(false); + const [cleanupFailed, setCleanupFailed] = useState(false); + const [removedDashboardUrl, setRemovedDashboardUrl] = useState(null); + const requestGeneration = useRef(0); + const tokenInputRef = useRef(null); + const connectionTitleRef = useRef(null); + + const removeMutationSuccess = () => { + setRemoveOpen(false); + setCleanupFailed(false); + setRemovedDashboardUrl(current => current ?? 'https://vercel.com/dashboard'); + toast.success('Vercel compute credentials removed'); + }; + const removeMutationError = (error: VercelComputeRemoveError) => { + if (error.data?.code === 'PRECONDITION_FAILED') setCleanupFailed(true); + toast.error(error.message || 'Could not remove credentials'); + }; + const normalizedToken = token.trim(); + const { + enrollmentState, + isEnrolled, + status, + statusQuery, + isSetupFormVisible, + discoverTeamsMutation, + discoverProjectsMutation, + addMutation, + retryMutation, + upgradeMutation, + removeMutation, + teams, + projects, + dispatchDiscoverTeams, + dispatchDiscoverProjects, + dispatchAdd, + dispatchRetrySetup, + dispatchUpgradeRuntime, + dispatchRemove, + } = useVercelComputeData(organizationId, normalizedToken, teamId, { + onRemoveSuccess: removeMutationSuccess, + onRemoveError: removeMutationError, + }); + + const resetTeamsMutation = discoverTeamsMutation.reset; + const resetProjectsMutation = discoverProjectsMutation.reset; + const resetAddMutation = addMutation.reset; + + useLayoutEffect(() => { + setToken(''); + setTeamId(''); + setProjectId(''); + setSetupError(null); + + return () => { + requestGeneration.current += 1; + resetTeamsMutation(); + resetProjectsMutation(); + resetAddMutation(); + }; + }, [ + organizationId, + isSetupFormVisible, + resetTeamsMutation, + resetProjectsMutation, + resetAddMutation, + ]); + + const isDiscovering = discoverTeamsMutation.isPending || discoverProjectsMutation.isPending; + const isBusy = + addMutation.isPending || + retryMutation.isPending || + upgradeMutation.isPending || + removeMutation.isPending; + const isFormBusy = isBusy || isDiscovering; + const hasTeams = !!teams?.length; + + useLayoutEffect(() => { + if (hasTeams) connectionTitleRef.current?.focus(); + else tokenInputRef.current?.focus(); + }, [hasTeams]); + + const selectedTeam = teams?.find(team => team.id === teamId); + const selectedProject = projects?.find( + project => project.id === projectId && project.teamId === teamId + ); + const canStartSetup = + !isFormBusy && isSetupFormVisible && !!normalizedToken && !!selectedTeam && !!selectedProject; + + function resetDiscovery() { + requestGeneration.current += 1; + setTeamId(''); + setProjectId(''); + resetTeamsMutation(); + resetProjectsMutation(); + resetAddMutation(); + } + + async function loadProjects(nextTeamId: string) { + if (isFormBusy || !isSetupFormVisible || !normalizedToken || !nextTeamId) return; + + const generation = ++requestGeneration.current; + setTeamId(nextTeamId); + setProjectId(''); + setSetupError(null); + resetProjectsMutation(); + resetAddMutation(); + + try { + const discoveredProjects = await dispatchDiscoverProjects({ + token: normalizedToken, + teamId: nextTeamId, + }); + if (generation !== requestGeneration.current) return; + + const onlyProject = discoveredProjects.length === 1 ? discoveredProjects[0] : undefined; + if (onlyProject) setProjectId(onlyProject.id); + } catch { + return; + } + } + + async function discoverTeams() { + if (isFormBusy || !isSetupFormVisible || !normalizedToken) return; + + resetDiscovery(); + const generation = requestGeneration.current; + setSetupError(null); + + try { + const discoveredTeams = await dispatchDiscoverTeams({ token: normalizedToken }); + if (generation !== requestGeneration.current) return; + + const onlyTeam = discoveredTeams.length === 1 ? discoveredTeams[0] : undefined; + if (onlyTeam) await loadProjects(onlyTeam.id); + } catch { + return; + } + } + + async function submit(event: FormEvent) { + event.preventDefault(); + if (!hasTeams) { + await discoverTeams(); + return; + } + if (!canStartSetup || !selectedTeam || !selectedProject) return; + + const generation = ++requestGeneration.current; + setSetupError(null); + + try { + await dispatchAdd({ + token: normalizedToken, + teamId: selectedTeam.id, + projectId: selectedProject.id, + }); + } catch (error) { + if (generation === requestGeneration.current) { + const message = + error instanceof Error && error.message + ? error.message + : 'Could not start Vercel compute setup'; + setSetupError(message); + toast.error(message); + } + return; + } finally { + if (generation === requestGeneration.current) resetAddMutation(); + } + + if (generation !== requestGeneration.current) return; + + setToken(''); + resetDiscovery(); + setSetupError(null); + toast.success('Vercel compute setup started'); + } + + function requestRemove() { + setCleanupFailed(false); + setRemovedDashboardUrl(dashboardUrl(status?.teamSlug ?? null, status?.projectSlug ?? null)); + setRemoveOpen(true); + } + + const isUpgradeRunning = + status?.upgradeStatus === 'pending' || status?.upgradeStatus === 'building'; + const setupStatus = status + ? isUpgradeRunning + ? { label: 'Upgrading', variant: 'secondary' as const } + : statusCopy(status.setupStatus, enrollmentState) + : null; + const progressStep = isUpgradeRunning ? status?.upgradeStep : status?.setupStep; + const currentStep = progressStep ? SETUP_STEPS.findIndex(([step]) => step === progressStep) : -1; + const projectLink = dashboardUrl(status?.teamSlug ?? null, status?.projectSlug ?? null); + + return ( +
+
+
+

Vercel compute

+

+ Run Cloud Agent on your {organizationId !== undefined ? "team's" : 'own'} Vercel + account. +

+
+ + {enrollmentState === 'checking' && ( + + + Checking Vercel compute availability + + Setup stays unavailable until enrollment is confirmed. + + + )} + + {enrollmentState === 'unavailable' && ( + + + Vercel compute availability could not be verified + + Setup is temporarily unavailable. Try again later. Existing credentials can still be + removed. + + + )} + + {enrollmentState === 'not-enrolled' && ( + + + Customer-paid Vercel is not enabled + + This owner is not enrolled in customer-paid Vercel compute. Existing credentials can + still be removed. + + + )} + + {statusQuery.isError && ( + + + Vercel compute status could not load + +

Try loading the status again.

+ +
+
+ )} + + {isSetupFormVisible ? ( + + + + {hasTeams ? 'Choose a project' : 'Connect your account'} + + {hasTeams && ( + + )} + + +
+ {!hasTeams ? ( +
+
+ + +
+ { + if (isFormBusy) return; + setToken(event.target.value); + resetDiscovery(); + setSetupError(null); + }} + placeholder="Paste your access token" + required + disabled={isFormBusy} + className="h-control-touch sm:h-9" + aria-invalid={discoverTeamsMutation.isError} + aria-describedby={ + discoverTeamsMutation.isError + ? 'vercel-token-help vercel-teams-error' + : 'vercel-token-help vercel-teams-status' + } + /> +

+ Use a team- or project-scoped Vercel token. +

+

+ {discoverTeamsMutation.isPending + ? 'Finding your Vercel teams.' + : teams?.length === 0 + ? 'No teams found. Check the token’s team access and try again.' + : null} +

+ {discoverTeamsMutation.isError && ( + + )} +
+ ) : ( + <> +
+
+ + +
+
+ + +

+ {discoverProjectsMutation.isPending + ? 'Loading projects for the selected team.' + : projects?.length === 0 + ? 'No projects found. Choose another team or check token access.' + : null} +

+ {discoverProjectsMutation.isError && ( + + )} + {(discoverProjectsMutation.isError || projects?.length === 0) && ( + + )} +
+
+
    +
  • Vercel bills your team for setup and sessions.
  • +
  • Setup does a one-time build in your Vercel project.
  • +
  • Your Vercel token is stored encrypted when you start setup.
  • +
+ + )} + {setupError && ( +

+ {setupError} +

+ )} +
+ +
+
+
+
+ ) : status ? ( + <> + + +
+ Vercel runtime + + {status.setupStatus === 'ready' + ? isEnrolled + ? 'New sessions are ready to use this customer-paid project.' + : enrollmentState === 'checking' + ? 'Checking whether new sessions can use this configured project.' + : enrollmentState === 'unavailable' + ? 'Enrollment could not be verified. This project cannot be confirmed as active.' + : 'This owner is not enrolled. New sessions will not use this project.' + : 'Setup status is updated automatically while the runtime is built.'} + +
+
+ {setupStatus && {setupStatus.label}} + + + + + + + Remove credentials + + + +
+
+ + {status.setupStatus === 'ready' ? ( +
+
+
+

+ {projectLabel(status.teamSlug, status.projectSlug, status.projectId)} +

+

+ Version{' '} + {formatRuntimeLabel( + status.runtimeWrapperVersion, + status.runtimeReleasedAt + )} +

+ {status.upgradeAvailable && + !isUpgradeRunning && + status.upgradeStatus !== 'failed' && ( +

+ A newer runtime is available. +

+ )} +
+ {projectLink && ( + + )} +
+ {isUpgradeRunning && ( +
+ {SETUP_STEPS.map(([step, label], index) => { + const complete = index < currentStep; + const active = index === currentStep; + return ( +
+ + {complete ? ( + + ) : active ? ( + + ) : ( + + )} + + + {label} + +
+ ); + })} +

+ Open sessions keep the current snapshot. New sessions use it until this + upgrade finishes. +

+
+ )} + {status.upgradeStatus === 'failed' && ( + + + Runtime upgrade could not finish + + The current snapshot is still in use. Check Vercel capacity and token + access, then retry the upgrade. + + + )} +
+ ) : status.setupStatus === 'failed' ? ( + + + Setup could not finish + + {status.setupError === 'byoc_vercel_forbidden' + ? 'The token cannot access this team or project.' + : status.setupError === 'byoc_vercel_capacity' + ? 'Vercel rejected the request because of a capacity or spend limit.' + : status.setupError === 'byoc_vercel_snapshot_missing' + ? 'The runtime snapshot expired or was deleted. Retry setup to rebuild it.' + : 'Check the token, team, and project access, then retry setup.'} + + + ) : ( +
+ {SETUP_STEPS.map(([step, label], index) => { + const complete = status.setupStatus === 'ready' || index < currentStep; + const active = status.setupStatus !== 'failed' && index === currentStep; + return ( +
+ + {complete ? ( + + ) : active ? ( + + ) : ( + + )} + + + {label} + +
+ ); + })} +
+ )} + + {status.setupStatus === 'failed' && ( +
+ +
+ )} + {status.setupStatus === 'ready' && + (status.upgradeStatus === 'failed' || + (status.upgradeAvailable && !isUpgradeRunning)) && ( +
+ {status.upgradeStatus === 'failed' && ( + + )} + {status.upgradeAvailable && + !isUpgradeRunning && + status.upgradeStatus !== 'failed' && ( + + )} +
+ )} +
+
+ + {((status.setupStatus !== 'ready' && status.setupStatus !== 'failed') || + isUpgradeRunning) && ( +

+ You can safely close this page. Setup continues in the background. +

+ )} + + ) : null} + + {removedDashboardUrl !== null && !status && ( + + + Credentials removed + + Kilo no longer has authority over existing Vercel sandboxes or snapshots. Review or + remove them in Vercel before starting another Cloud Agent session. + + + + + + + )} +
+ + { + if (removeMutation.isPending) return; + setRemoveOpen(open); + if (!open) setCleanupFailed(false); + }} + > + + + Remove Vercel credentials? + + + Kilo first attempts to remove the runtime snapshot and setup resources. Once the + credentials are deleted, Kilo can no longer start, inspect, extend, or stop your + Vercel sandboxes. Existing Cloud Agent sessions using this project will stop + working. + + + Review remaining snapshots and running sandboxes in Vercel before continuing. Open{' '} + Observability → Sandboxes in the project after removal. You may + also revoke the token directly in Vercel. + + {cleanupFailed && ( + + Automatic cleanup could not be confirmed. Removing credentials now may leave + billable Vercel snapshots or sandboxes that Kilo can no longer remove. + + )} + {removedDashboardUrl && ( + + Open the configured Vercel project + + )} + + + + Cancel + dispatchRemove({ acknowledgeCleanupFailure: cleanupFailed })} + > + {removeMutation.isPending && } + {cleanupFailed ? 'Remove without cleanup' : 'Remove credentials'} + + + + +
+ ); +} diff --git a/apps/web/src/components/cloud-agent-next/settings/settings-options.test.ts b/apps/web/src/components/cloud-agent-next/settings/settings-options.test.ts index e7a44e9c8df..946fe2f293b 100644 --- a/apps/web/src/components/cloud-agent-next/settings/settings-options.test.ts +++ b/apps/web/src/components/cloud-agent-next/settings/settings-options.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect } from '@jest/globals'; import { - getKiloSandboxAllocation, + getSandboxAllocation, getSandboxAllocationRequest, type SandboxSelectionCapabilities, } from '@kilocode/worker-utils/sandbox-allocation'; @@ -44,7 +44,7 @@ describe('settings-options', () => { test('names a provider-chosen default without a capacity', () => { expect( describeDefaultSandbox({ - provider: { id: 'vercel', account: 'kilo' }, + provider: { id: 'vercel', account: 'byoc' }, instanceType: 'default', }) ).toEqual({ summary: 'Vercel · Provider default', full: 'Vercel · Provider default' }); @@ -168,6 +168,6 @@ describe('settings-options', () => { test('the chosen key round-trips through the capability option', () => { const option = capabilities().options[0]; - expect(getKiloSandboxAllocation(option.allocation)).toBe('cloudflare-single'); + expect(getSandboxAllocation(option.allocation)).toBe('cloudflare-single'); }); }); diff --git a/apps/web/src/components/cloud-agent-next/settings/settings-options.ts b/apps/web/src/components/cloud-agent-next/settings/settings-options.ts index 498168fa7b9..dd38baf042c 100644 --- a/apps/web/src/components/cloud-agent-next/settings/settings-options.ts +++ b/apps/web/src/components/cloud-agent-next/settings/settings-options.ts @@ -1,6 +1,6 @@ import { SELECTABLE_SANDBOX_ALLOCATIONS, - getKiloSandboxAllocation, + getSandboxAllocation, getSandboxAllocationRequest, isSelectableSandboxAllocation, type SandboxDestination, @@ -63,7 +63,7 @@ export function getEligibleAllocationChoices( capabilities: SandboxSelectionCapabilities | undefined ): SandboxAllocationChoice[] { return getSandboxSelectionOptions(capabilities).flatMap(option => { - const allocation = getKiloSandboxAllocation(option.allocation); + const allocation = getSandboxAllocation(option.allocation); return allocation && isSelectableSandboxAllocation(allocation) ? [toChoice(allocation, option.allocation)] : []; diff --git a/apps/web/src/hooks/useVercelComputeData.ts b/apps/web/src/hooks/useVercelComputeData.ts new file mode 100644 index 00000000000..953adda3787 --- /dev/null +++ b/apps/web/src/hooks/useVercelComputeData.ts @@ -0,0 +1,291 @@ +'use client'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import type { inferRouterOutputs } from '@trpc/server'; +import { toast } from 'sonner'; +import type { RootRouter } from '@/routers/root-router'; +import { useTRPC, useRawTRPCClient } from '@/lib/trpc/utils'; +import type { VercelProject, VercelTeam } from '@/lib/vercel-client'; + +export type VercelComputeRemoveError = { + data?: { code?: string } | null; + message: string; +}; + +type VercelComputeDataCallbacks = { + onRemoveSuccess: () => void; + onRemoveError: (error: VercelComputeRemoveError) => void; +}; + +type VercelComputeDiscoveryInput = { + token: string; +}; + +type VercelComputeProjectsInput = { + token: string; + teamId: string; +}; + +type VercelComputeSetupInput = { + token: string; + teamId: string; + projectId: string; +}; + +type VercelComputeRemoveInput = { + acknowledgeCleanupFailure: boolean; +}; + +type VercelComputeDiscoveryMutationInput = + | VercelComputeDiscoveryInput + | (VercelComputeDiscoveryInput & { organizationId: string }); + +type VercelComputeProjectsMutationInput = + | VercelComputeProjectsInput + | (VercelComputeProjectsInput & { organizationId: string }); + +type VercelComputeSetupMutationInput = + | VercelComputeSetupInput + | (VercelComputeSetupInput & { organizationId: string }); + +type VercelComputeRetryMutationInput = { organizationId: string } | undefined; + +type VercelComputeRemoveMutationInput = + | VercelComputeRemoveInput + | (VercelComputeRemoveInput & { organizationId: string }); + +type VercelComputeStatus = NonNullable< + inferRouterOutputs['vercelCompute']['getStatus'] +>; + +function matchesOwner( + variables: { token: string; organizationId?: string }, + organizationId: string | undefined +): boolean { + return variables.organizationId === organizationId; +} + +export function useVercelComputeData( + organizationId: string | undefined, + normalizedToken: string, + teamId: string, + { onRemoveSuccess, onRemoveError }: VercelComputeDataCallbacks +) { + const trpc = useTRPC(); + const trpcClient = useRawTRPCClient(); + const queryClient = useQueryClient(); + const organizationOwner = organizationId !== undefined; + + const enrollmentQuery = useQuery( + organizationOwner + ? trpc.organizations.vercelCompute.getEnrollment.queryOptions({ organizationId }) + : trpc.vercelCompute.getEnrollment.queryOptions() + ); + const enrollmentState: 'checking' | 'unavailable' | 'enrolled' | 'not-enrolled' = + enrollmentQuery.isPending + ? 'checking' + : enrollmentQuery.isError + ? 'unavailable' + : enrollmentQuery.data?.enrolled + ? 'enrolled' + : 'not-enrolled'; + const isEnrolled = enrollmentState === 'enrolled'; + + const statusQuery = useQuery({ + ...(organizationOwner + ? trpc.organizations.vercelCompute.getStatus.queryOptions({ organizationId }) + : trpc.vercelCompute.getStatus.queryOptions()), + refetchInterval: query => + query.state.data?.setupStatus === 'pending' || + query.state.data?.setupStatus === 'building' || + query.state.data?.upgradeStatus === 'pending' || + query.state.data?.upgradeStatus === 'building' + ? 2_000 + : false, + }); + const status = statusQuery.data; + const isSetupFormVisible = !status && statusQuery.isSuccess && isEnrolled; + + const invalidateStatus = () => + queryClient.invalidateQueries({ + queryKey: organizationOwner + ? trpc.organizations.vercelCompute.getStatus.queryKey({ organizationId }) + : trpc.vercelCompute.getStatus.queryKey(), + }); + + const discoverTeamsMutation = useMutation< + VercelTeam[], + Error, + VercelComputeDiscoveryMutationInput + >({ + mutationKey: organizationOwner + ? trpc.organizations.vercelCompute.discoverTeams.mutationKey() + : trpc.vercelCompute.discoverTeams.mutationKey(), + gcTime: 0, + mutationFn: input => { + if (organizationId !== undefined) { + return trpcClient.organizations.vercelCompute.discoverTeams.mutate({ + organizationId, + token: input.token, + }); + } + return trpcClient.vercelCompute.discoverTeams.mutate({ token: input.token }); + }, + }); + const discoverProjectsMutation = useMutation< + VercelProject[], + Error, + VercelComputeProjectsMutationInput + >({ + mutationKey: organizationOwner + ? trpc.organizations.vercelCompute.discoverProjects.mutationKey() + : trpc.vercelCompute.discoverProjects.mutationKey(), + gcTime: 0, + mutationFn: input => { + if (organizationId !== undefined) { + return trpcClient.organizations.vercelCompute.discoverProjects.mutate({ + organizationId, + token: input.token, + teamId: input.teamId, + }); + } + return trpcClient.vercelCompute.discoverProjects.mutate({ + token: input.token, + teamId: input.teamId, + }); + }, + }); + const addMutation = useMutation({ + mutationKey: organizationOwner + ? trpc.organizations.vercelCompute.add.mutationKey() + : trpc.vercelCompute.add.mutationKey(), + gcTime: 0, + mutationFn: input => { + if (organizationId !== undefined) { + return trpcClient.organizations.vercelCompute.add.mutate({ + organizationId, + token: input.token, + teamId: input.teamId, + projectId: input.projectId, + }); + } + return trpcClient.vercelCompute.add.mutate({ + token: input.token, + teamId: input.teamId, + projectId: input.projectId, + }); + }, + onSettled: () => void invalidateStatus(), + }); + const mutationSuccess = (message: string) => () => { + toast.success(message); + void invalidateStatus(); + }; + const retryMutation = useMutation({ + mutationKey: organizationOwner + ? trpc.organizations.vercelCompute.retrySetup.mutationKey() + : trpc.vercelCompute.retrySetup.mutationKey(), + mutationFn: _input => { + if (organizationId !== undefined) { + return trpcClient.organizations.vercelCompute.retrySetup.mutate({ organizationId }); + } + return trpcClient.vercelCompute.retrySetup.mutate(); + }, + onSuccess: mutationSuccess('Vercel compute setup restarted'), + onError: error => toast.error(error.message || 'Could not retry setup'), + }); + const upgradeMutation = useMutation({ + mutationKey: organizationOwner + ? trpc.organizations.vercelCompute.upgradeRuntime.mutationKey() + : trpc.vercelCompute.upgradeRuntime.mutationKey(), + mutationFn: _input => { + if (organizationId !== undefined) { + return trpcClient.organizations.vercelCompute.upgradeRuntime.mutate({ organizationId }); + } + return trpcClient.vercelCompute.upgradeRuntime.mutate(); + }, + onSuccess: mutationSuccess('Runtime upgrade started'), + onError: error => toast.error(error.message || 'Could not start runtime upgrade'), + }); + const removeMutation = useMutation<{ success: true }, Error, VercelComputeRemoveMutationInput>({ + mutationKey: organizationOwner + ? trpc.organizations.vercelCompute.remove.mutationKey() + : trpc.vercelCompute.remove.mutationKey(), + mutationFn: input => { + if (organizationId !== undefined) { + return trpcClient.organizations.vercelCompute.remove.mutate({ + organizationId, + acknowledgeCleanupFailure: input.acknowledgeCleanupFailure, + }); + } + return trpcClient.vercelCompute.remove.mutate({ + acknowledgeCleanupFailure: input.acknowledgeCleanupFailure, + }); + }, + onSuccess: () => { + onRemoveSuccess(); + void invalidateStatus(); + }, + onError: onRemoveError, + }); + + const teamsVariables = discoverTeamsMutation.variables; + const teams = + teamsVariables !== undefined && + matchesOwner(teamsVariables, organizationId) && + teamsVariables.token === normalizedToken + ? discoverTeamsMutation.data + : undefined; + const projectsVariables = discoverProjectsMutation.variables; + const projects = + projectsVariables !== undefined && + matchesOwner(projectsVariables, organizationId) && + projectsVariables.token === normalizedToken && + projectsVariables.teamId === teamId + ? discoverProjectsMutation.data + : undefined; + + const dispatchDiscoverTeams = (input: VercelComputeDiscoveryInput) => + organizationOwner + ? discoverTeamsMutation.mutateAsync({ organizationId, ...input }) + : discoverTeamsMutation.mutateAsync(input); + const dispatchDiscoverProjects = (input: VercelComputeProjectsInput) => + organizationOwner + ? discoverProjectsMutation.mutateAsync({ organizationId, ...input }) + : discoverProjectsMutation.mutateAsync(input); + const dispatchAdd = (input: VercelComputeSetupInput) => + organizationOwner + ? addMutation.mutateAsync({ organizationId, ...input }) + : addMutation.mutateAsync(input); + const dispatchRetrySetup = () => + organizationOwner ? retryMutation.mutate({ organizationId }) : retryMutation.mutate(undefined); + const dispatchUpgradeRuntime = () => + organizationOwner + ? upgradeMutation.mutate({ organizationId }) + : upgradeMutation.mutate(undefined); + const dispatchRemove = (input: VercelComputeRemoveInput) => + organizationOwner + ? removeMutation.mutate({ organizationId, ...input }) + : removeMutation.mutate(input); + + return { + enrollmentState, + isEnrolled, + status, + statusQuery, + isSetupFormVisible, + discoverTeamsMutation, + discoverProjectsMutation, + addMutation, + retryMutation, + upgradeMutation, + removeMutation, + teams, + projects, + dispatchDiscoverTeams, + dispatchDiscoverProjects, + dispatchAdd, + dispatchRetrySetup, + dispatchUpgradeRuntime, + dispatchRemove, + }; +} diff --git a/apps/web/src/lib/cloud-agent-next/cloud-agent-client.test.ts b/apps/web/src/lib/cloud-agent-next/cloud-agent-client.test.ts index 980d2119863..0487527317b 100644 --- a/apps/web/src/lib/cloud-agent-next/cloud-agent-client.test.ts +++ b/apps/web/src/lib/cloud-agent-next/cloud-agent-client.test.ts @@ -107,6 +107,10 @@ const { CloudAgentNextClient, createSkippedBalanceCheckCloudAgentNextClient, readCloudAgentWidgetApprovalKey, + getVercelComputeEnrollment, + getVercelComputeEnrollmentForUser, + getVercelRuntimeIdentity, + cleanupVercelSnapshotBuild, } = realCloudAgentClientModule; describe('CloudAgentNextClient review message results', () => { @@ -549,7 +553,7 @@ describe('CloudAgentNextClient sandbox selection', () => { const capabilities: SandboxSelectionCapabilities = { enabled: true, defaultDestination: { - provider: { id: 'vercel', account: 'kilo' }, + provider: { id: 'vercel', account: 'byoc' }, instanceType: 'default', }, options: [ @@ -1251,3 +1255,144 @@ describe('readCloudAgentWidgetApprovalKey', () => { ); }); }); + +describe('getVercelComputeEnrollment', () => { + const originalFetch = global.fetch; + + afterEach(() => { + global.fetch = originalFetch; + }); + + it.each([true, false])('returns the authenticated Worker enrollment value %s', async enrolled => { + const fetchMock = jest + .fn<(input: RequestInfo | URL, init?: RequestInit) => Promise>() + .mockResolvedValue(Response.json({ enrolled })); + global.fetch = fetchMock as unknown as typeof fetch; + + await expect(getVercelComputeEnrollment('org/with-path')).resolves.toBe(enrolled); + expect(fetchMock).toHaveBeenCalledWith( + 'http://cloud-agent-next/internal/byoc/vercel-enrollment/org%2Fwith-path', + expect.objectContaining({ + headers: { 'x-internal-api-key': 'test-secret' }, + cache: 'no-store', + }) + ); + }); + + it('fails closed when the Worker response is malformed', async () => { + const fetchMock = jest + .fn<(input: RequestInfo | URL, init?: RequestInit) => Promise>() + .mockResolvedValue(Response.json({ enrolled: 'true' })); + global.fetch = fetchMock as unknown as typeof fetch; + + await expect(getVercelComputeEnrollment('org_1')).rejects.toThrow( + 'Cloud Agent Vercel compute enrollment could not be verified' + ); + }); + + it('does not expose upstream response details when enrollment cannot be verified', async () => { + const fetchMock = jest + .fn<(input: RequestInfo | URL, init?: RequestInit) => Promise>() + .mockResolvedValue(new Response('sensitive upstream details', { status: 502 })); + global.fetch = fetchMock as unknown as typeof fetch; + + await expect(getVercelComputeEnrollment('org_1')).rejects.toThrow( + 'Cloud Agent Vercel compute enrollment could not be verified' + ); + }); + + it('encodes OAuth user ids on the personal enrollment endpoint', async () => { + const fetchMock = jest + .fn<(input: RequestInfo | URL, init?: RequestInit) => Promise>() + .mockResolvedValue(Response.json({ enrolled: true })); + global.fetch = fetchMock as unknown as typeof fetch; + + await expect(getVercelComputeEnrollmentForUser('oauth/provider/user id')).resolves.toBe(true); + expect(fetchMock).toHaveBeenCalledWith( + 'http://cloud-agent-next/internal/byoc/vercel-enrollment/user/oauth%2Fprovider%2Fuser%20id', + expect.objectContaining({ + headers: { 'x-internal-api-key': 'test-secret' }, + cache: 'no-store', + }) + ); + }); +}); + +describe('getVercelRuntimeIdentity', () => { + const originalFetch = global.fetch; + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('returns the Worker runtime identity', async () => { + const identity = { + wrapperVersion: '2.4.0', + releasedAt: '2026-09-08', + digest: 'digest-1', + }; + const fetchMock = jest + .fn<(input: RequestInfo | URL, init?: RequestInit) => Promise>() + .mockResolvedValue(Response.json(identity)); + global.fetch = fetchMock as unknown as typeof fetch; + + await expect(getVercelRuntimeIdentity()).resolves.toEqual(identity); + expect(fetchMock).toHaveBeenCalledWith( + 'http://cloud-agent-next/internal/byoc/vercel-runtime-identity', + expect.objectContaining({ + headers: { 'x-internal-api-key': 'test-secret' }, + cache: 'no-store', + }) + ); + }); +}); + +describe('cleanupVercelSnapshotBuild', () => { + const originalFetch = global.fetch; + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('sends the persisted snapshot identifier to the authenticated cleanup endpoint', async () => { + const fetchMock = jest + .fn<(input: RequestInfo | URL, init?: RequestInit) => Promise>() + .mockResolvedValue(new Response(null, { status: 204 })); + global.fetch = fetchMock as unknown as typeof fetch; + const input = { + organizationId: 'org_1', + credentialId: 'credential_1', + buildGeneration: 'generation_1', + snapshotId: 'snap_existing', + }; + + await cleanupVercelSnapshotBuild(input); + + expect(fetchMock).toHaveBeenCalledWith( + 'http://cloud-agent-next/internal/byoc/vercel-snapshot-build/cleanup', + expect.objectContaining({ + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-internal-api-key': 'test-secret', + }, + body: JSON.stringify(input), + }) + ); + }); + + it('fails safely when cleanup is rejected', async () => { + const fetchMock = jest + .fn<(input: RequestInfo | URL, init?: RequestInit) => Promise>() + .mockResolvedValue(new Response('sensitive provider details', { status: 502 })); + global.fetch = fetchMock as unknown as typeof fetch; + + await expect( + cleanupVercelSnapshotBuild({ + organizationId: 'org_1', + credentialId: 'credential_1', + buildGeneration: 'generation_1', + }) + ).rejects.toThrow('Cloud Agent Vercel compute cleanup could not be completed'); + }); +}); diff --git a/apps/web/src/lib/cloud-agent-next/cloud-agent-client.ts b/apps/web/src/lib/cloud-agent-next/cloud-agent-client.ts index 5deb0ea5926..bbb309e9885 100644 --- a/apps/web/src/lib/cloud-agent-next/cloud-agent-client.ts +++ b/apps/web/src/lib/cloud-agent-next/cloud-agent-client.ts @@ -1335,3 +1335,173 @@ export async function readCloudAgentWidgetApprovalKey( const parsed = widgetApprovalKeyResponseSchema.parse(await response.json()); return { approvalKey: parsed.approvalKey, command: parsed.command ?? null }; } + +export type VercelSnapshotBuildStartInput = ( + | { organizationId: string; userId?: never } + | { userId: string; organizationId?: never } +) & { + credentialId: string; + buildGeneration: string; +}; + +/** + * Start an organization-scoped BYOC Vercel snapshot build through the + * authenticated Cloud Agent internal API. This is intentionally not part of + * the browser-facing tRPC client. + */ +export async function startVercelSnapshotBuild( + input: VercelSnapshotBuildStartInput +): Promise { + let response: Response; + try { + response = await fetch( + `${CLOUD_AGENT_NEXT_API_URL}/internal/byoc/vercel-snapshot-build/start`, + { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-internal-api-key': INTERNAL_API_SECRET, + }, + body: JSON.stringify(input), + signal: AbortSignal.timeout(30_000), + } + ); + } catch { + throw new Error('Cloud Agent snapshot build request failed'); + } + + if (!response.ok) { + throw new Error('Cloud Agent snapshot build request was rejected'); + } +} + +export type VercelRuntimeIdentity = { + wrapperVersion: string; + releasedAt: string; + digest: string; +}; + +export async function getVercelRuntimeIdentity(): Promise { + let response: Response; + try { + response = await fetch(`${CLOUD_AGENT_NEXT_API_URL}/internal/byoc/vercel-runtime-identity`, { + headers: { 'x-internal-api-key': INTERNAL_API_SECRET }, + cache: 'no-store', + signal: AbortSignal.timeout(30_000), + }); + } catch { + throw new Error('Cloud Agent runtime identity could not be verified'); + } + + if (!response.ok) { + throw new Error('Cloud Agent runtime identity could not be verified'); + } + + const body: unknown = await response.json().catch(() => null); + const parsed = z + .object({ + wrapperVersion: z.string().min(1), + releasedAt: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), + digest: z.string().min(1), + }) + .safeParse(body); + if (!parsed.success) { + throw new Error('Cloud Agent runtime identity could not be verified'); + } + return parsed.data; +} + +export async function getVercelComputeEnrollment(organizationId: string): Promise { + let response: Response; + try { + response = await fetch( + `${CLOUD_AGENT_NEXT_API_URL}/internal/byoc/vercel-enrollment/${encodeURIComponent(organizationId)}`, + { + headers: { 'x-internal-api-key': INTERNAL_API_SECRET }, + cache: 'no-store', + signal: AbortSignal.timeout(30_000), + } + ); + } catch { + throw new Error('Cloud Agent Vercel compute enrollment could not be verified'); + } + + if (!response.ok) { + throw new Error('Cloud Agent Vercel compute enrollment could not be verified'); + } + + const body: unknown = await response.json().catch(() => null); + if ( + typeof body !== 'object' || + body === null || + !('enrolled' in body) || + typeof body.enrolled !== 'boolean' + ) { + throw new Error('Cloud Agent Vercel compute enrollment could not be verified'); + } + + return body.enrolled; +} + +export async function getVercelComputeEnrollmentForUser(userId: string): Promise { + let response: Response; + try { + response = await fetch( + `${CLOUD_AGENT_NEXT_API_URL}/internal/byoc/vercel-enrollment/user/${encodeURIComponent(userId)}`, + { + headers: { 'x-internal-api-key': INTERNAL_API_SECRET }, + cache: 'no-store', + signal: AbortSignal.timeout(30_000), + } + ); + } catch { + throw new Error('Cloud Agent Vercel compute enrollment could not be verified'); + } + + if (!response.ok) { + throw new Error('Cloud Agent Vercel compute enrollment could not be verified'); + } + + const body: unknown = await response.json().catch(() => null); + if ( + typeof body !== 'object' || + body === null || + !('enrolled' in body) || + typeof body.enrolled !== 'boolean' + ) { + throw new Error('Cloud Agent Vercel compute enrollment could not be verified'); + } + + return body.enrolled; +} + +/** Exactly one of organizationId or userId must be set. */ +export type VercelSnapshotBuildCleanupInput = VercelSnapshotBuildStartInput & { + snapshotId?: string; +}; + +export async function cleanupVercelSnapshotBuild( + input: VercelSnapshotBuildCleanupInput +): Promise { + let response: Response; + try { + response = await fetch( + `${CLOUD_AGENT_NEXT_API_URL}/internal/byoc/vercel-snapshot-build/cleanup`, + { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-internal-api-key': INTERNAL_API_SECRET, + }, + body: JSON.stringify(input), + signal: AbortSignal.timeout(30_000), + } + ); + } catch { + throw new Error('Cloud Agent Vercel compute cleanup could not be completed'); + } + + if (!response.ok) { + throw new Error('Cloud Agent Vercel compute cleanup could not be completed'); + } +} diff --git a/apps/web/src/lib/cloud-agent-next/sandbox-allocation-offers.ts b/apps/web/src/lib/cloud-agent-next/sandbox-allocation-offers.ts index 6d2973f6179..680ff9759ba 100644 --- a/apps/web/src/lib/cloud-agent-next/sandbox-allocation-offers.ts +++ b/apps/web/src/lib/cloud-agent-next/sandbox-allocation-offers.ts @@ -1,7 +1,7 @@ import 'server-only'; import { TRPCError } from '@trpc/server'; import { - getKiloSandboxAllocation, + getSandboxAllocation, type SandboxSelectionCapabilities, } from '@kilocode/worker-utils/sandbox-allocation'; import type { User } from '@kilocode/db/schema'; @@ -41,7 +41,7 @@ export function findSandboxOption( allocation: string ): SandboxSelectionCapabilities['options'][number] | undefined { return capabilities.options.find( - option => getKiloSandboxAllocation(option.allocation) === allocation + option => getSandboxAllocation(option.allocation) === allocation ); } diff --git a/apps/web/src/lib/cloud-agent-next/vercel-compute-status.test.ts b/apps/web/src/lib/cloud-agent-next/vercel-compute-status.test.ts new file mode 100644 index 00000000000..8a73efe84db --- /dev/null +++ b/apps/web/src/lib/cloud-agent-next/vercel-compute-status.test.ts @@ -0,0 +1,130 @@ +import { applyVercelComputeStatusProjection } from './vercel-compute-status'; + +describe('applyVercelComputeStatusProjection', () => { + const ready = { + setup_status: 'ready' as const, + setup_step: null, + setup_error: null, + team_slug: 'team', + project_slug: 'project', + runtime_build_id: 'runtime-old', + runtime_snapshot_id: 'snapshot-old', + runtime_wrapper_version: '2.4.0', + runtime_released_at: '2026-09-01', + runtime_digest: 'digest-old', + upgrade_status: null, + upgrade_step: null, + upgrade_error: null, + setup_started_at: '2026-09-01T00:00:00.000Z', + setup_completed_at: '2026-09-01T01:00:00.000Z', + }; + + const buildingProjection = { + setupStatus: 'building' as const, + setupStep: 'create_builder' as const, + setupError: null, + teamSlug: 'team', + projectSlug: 'project', + runtimeBuildId: 'runtime-new', + runtimeSnapshotId: null, + setupStartedAt: '2026-09-08T00:00:00.000Z', + setupCompletedAt: null, + runtimeWrapperVersion: null, + runtimeReleasedAt: null, + runtimeDigest: null, + }; + + it('keeps a Ready snapshot while an upgrade is building', () => { + expect(applyVercelComputeStatusProjection(ready, buildingProjection)).toMatchObject({ + setup_status: 'ready', + runtime_snapshot_id: 'snapshot-old', + runtime_digest: 'digest-old', + upgrade_status: 'building', + upgrade_step: 'create_builder', + setup_completed_at: ready.setup_completed_at, + }); + }); + + it('keeps a Ready snapshot when an upgrade build fails', () => { + expect( + applyVercelComputeStatusProjection(ready, { + ...buildingProjection, + setupStatus: 'failed', + setupStep: null, + setupError: 'provider_request_failed', + }) + ).toMatchObject({ + setup_status: 'ready', + runtime_snapshot_id: 'snapshot-old', + upgrade_status: 'failed', + upgrade_error: 'provider_request_failed', + }); + }); + + it('swaps the Ready snapshot when an upgrade becomes Ready', () => { + expect( + applyVercelComputeStatusProjection( + { ...ready, upgrade_status: 'building' }, + { + setupStatus: 'ready', + setupStep: null, + setupError: null, + teamSlug: 'team', + projectSlug: 'project', + runtimeBuildId: 'runtime-new', + runtimeSnapshotId: 'snapshot-new', + setupStartedAt: '2026-09-08T00:00:00.000Z', + setupCompletedAt: '2026-09-08T01:00:00.000Z', + runtimeWrapperVersion: '2.4.0', + runtimeReleasedAt: '2026-09-08', + runtimeDigest: 'digest-new', + } + ) + ).toMatchObject({ + setup_status: 'ready', + runtime_snapshot_id: 'snapshot-new', + runtime_digest: 'digest-new', + runtime_released_at: '2026-09-08', + upgrade_status: null, + setup_completed_at: '2026-09-08T01:00:00.000Z', + }); + }); + + it('fails the org when the live snapshot is missing', () => { + expect( + applyVercelComputeStatusProjection( + { ...ready, upgrade_status: 'building' }, + { + ...buildingProjection, + setupStatus: 'failed', + setupStep: null, + setupError: 'byoc_vercel_snapshot_missing', + } + ) + ).toMatchObject({ + setup_status: 'failed', + runtime_snapshot_id: null, + setup_error: 'byoc_vercel_snapshot_missing', + upgrade_status: null, + }); + }); + + it('applies first-time setup projections onto non-ready rows', () => { + expect( + applyVercelComputeStatusProjection( + { + ...ready, + setup_status: 'pending', + runtime_snapshot_id: null, + runtime_digest: null, + setup_completed_at: null, + }, + buildingProjection + ) + ).toMatchObject({ + setup_status: 'building', + runtime_snapshot_id: null, + upgrade_status: null, + }); + }); +}); diff --git a/apps/web/src/lib/cloud-agent-next/vercel-compute-status.ts b/apps/web/src/lib/cloud-agent-next/vercel-compute-status.ts new file mode 100644 index 00000000000..cb23896e20f --- /dev/null +++ b/apps/web/src/lib/cloud-agent-next/vercel-compute-status.ts @@ -0,0 +1,161 @@ +import type { + VercelComputeSetupStatus, + VercelComputeSetupStep, + VercelComputeUpgradeStatus, +} from '@kilocode/db/schema-types'; + +export type VercelComputeStatusProjection = { + setupStatus: VercelComputeSetupStatus; + setupStep: VercelComputeSetupStep | null; + setupError: string | null; + teamSlug: string | null; + projectSlug: string | null; + runtimeBuildId: string | null; + runtimeSnapshotId: string | null; + setupStartedAt: string | null; + setupCompletedAt: string | null; + runtimeWrapperVersion: string | null; + runtimeReleasedAt: string | null; + runtimeDigest: string | null; +}; + +export type VercelComputeStatusRow = { + setup_status: VercelComputeSetupStatus; + setup_step: VercelComputeSetupStep | null; + setup_error: string | null; + team_slug: string | null; + project_slug: string | null; + runtime_build_id: string | null; + runtime_snapshot_id: string | null; + runtime_wrapper_version: string | null; + runtime_released_at: string | null; + runtime_digest: string | null; + upgrade_status: VercelComputeUpgradeStatus | null; + upgrade_step: VercelComputeSetupStep | null; + upgrade_error: string | null; + setup_started_at: string | null; + setup_completed_at: string | null; +}; + +export type VercelComputeStatusColumns = { + setup_status: VercelComputeSetupStatus; + setup_step: VercelComputeSetupStep | null; + setup_error: string | null; + team_slug: string | null; + project_slug: string | null; + runtime_build_id: string | null; + runtime_snapshot_id: string | null; + runtime_wrapper_version: string | null; + runtime_released_at: string | null; + runtime_digest: string | null; + upgrade_status: VercelComputeUpgradeStatus | null; + upgrade_step: VercelComputeSetupStep | null; + upgrade_error: string | null; + setup_started_at: string | null; + setup_completed_at: string | null; +}; + +export function applyVercelComputeStatusProjection( + current: VercelComputeStatusRow, + data: VercelComputeStatusProjection +): VercelComputeStatusColumns { + if (data.setupStatus === 'failed' && data.setupError === 'byoc_vercel_snapshot_missing') { + return { + setup_status: 'failed', + setup_step: null, + setup_error: data.setupError, + team_slug: data.teamSlug, + project_slug: data.projectSlug, + runtime_build_id: data.runtimeBuildId, + runtime_snapshot_id: null, + runtime_wrapper_version: current.runtime_wrapper_version, + runtime_released_at: current.runtime_released_at, + runtime_digest: current.runtime_digest, + upgrade_status: null, + upgrade_step: null, + upgrade_error: null, + setup_started_at: data.setupStartedAt, + setup_completed_at: null, + }; + } + + if (current.setup_status === 'ready' && data.setupStatus === 'failed') { + return { + setup_status: 'ready', + setup_step: current.setup_step, + setup_error: current.setup_error, + team_slug: data.teamSlug ?? current.team_slug, + project_slug: data.projectSlug ?? current.project_slug, + runtime_build_id: current.runtime_build_id, + runtime_snapshot_id: current.runtime_snapshot_id, + runtime_wrapper_version: current.runtime_wrapper_version, + runtime_released_at: current.runtime_released_at, + runtime_digest: current.runtime_digest, + upgrade_status: 'failed', + upgrade_step: null, + upgrade_error: data.setupError, + setup_started_at: current.setup_started_at, + setup_completed_at: current.setup_completed_at, + }; + } + + if (current.setup_status === 'ready' && data.setupStatus !== 'ready') { + return { + setup_status: 'ready', + setup_step: current.setup_step, + setup_error: current.setup_error, + team_slug: data.teamSlug ?? current.team_slug, + project_slug: data.projectSlug ?? current.project_slug, + runtime_build_id: current.runtime_build_id, + runtime_snapshot_id: current.runtime_snapshot_id, + runtime_wrapper_version: current.runtime_wrapper_version, + runtime_released_at: current.runtime_released_at, + runtime_digest: current.runtime_digest, + upgrade_status: data.setupStatus === 'pending' ? 'pending' : 'building', + upgrade_step: data.setupStep, + upgrade_error: null, + setup_started_at: current.setup_started_at, + setup_completed_at: current.setup_completed_at, + }; + } + + if (current.setup_status === 'ready' && data.setupStatus === 'ready') { + return { + setup_status: 'ready', + setup_step: null, + setup_error: null, + team_slug: data.teamSlug, + project_slug: data.projectSlug, + runtime_build_id: data.runtimeBuildId, + runtime_snapshot_id: data.runtimeSnapshotId, + runtime_wrapper_version: data.runtimeWrapperVersion, + runtime_released_at: data.runtimeReleasedAt, + runtime_digest: data.runtimeDigest, + upgrade_status: null, + upgrade_step: null, + upgrade_error: null, + setup_started_at: current.setup_started_at, + setup_completed_at: data.setupCompletedAt, + }; + } + + return { + setup_status: data.setupStatus, + setup_step: data.setupStep, + setup_error: data.setupError, + team_slug: data.teamSlug, + project_slug: data.projectSlug, + runtime_build_id: data.runtimeBuildId, + runtime_snapshot_id: data.runtimeSnapshotId, + runtime_wrapper_version: + data.setupStatus === 'ready' ? data.runtimeWrapperVersion : current.runtime_wrapper_version, + runtime_released_at: + data.setupStatus === 'ready' ? data.runtimeReleasedAt : current.runtime_released_at, + runtime_digest: data.setupStatus === 'ready' ? data.runtimeDigest : current.runtime_digest, + upgrade_status: null, + upgrade_step: null, + upgrade_error: null, + setup_started_at: data.setupStartedAt, + setup_completed_at: data.setupCompletedAt, + }; +} diff --git a/apps/web/src/lib/user/index.test.ts b/apps/web/src/lib/user/index.test.ts index 0c78fe8f515..3065c98d84d 100644 --- a/apps/web/src/lib/user/index.test.ts +++ b/apps/web/src/lib/user/index.test.ts @@ -23,6 +23,7 @@ import { organization_groups, organization_group_memberships, organization_group_policy_settings, + organization_vercel_compute_credentials, organization_invitations, organization_recommendation_dismissals, security_audit_log, @@ -185,6 +186,7 @@ import { SecurityFindingNotificationStatus, UserDeletionCloudSubjectResolution, UserDeletionRequestStatus, + type VercelComputeCredentialEnvelope, } from '@kilocode/db/schema-types'; import type * as StripeClient from '@kilocode/web-shared/lib/stripe-client'; @@ -337,6 +339,7 @@ describe('User', () => { await db.delete(platform_access_token_credentials); await db.delete(platform_integrations); await db.delete(github_app_installations); + await db.delete(organization_vercel_compute_credentials); await db.delete(organizations); await db.delete(cli_sessions_v2); await db.delete(kilocode_users); @@ -2332,6 +2335,75 @@ describe('User', () => { ) ).toHaveLength(1); }); + + it('deletes personal Vercel credentials while preserving organization credentials', async () => { + const user = await insertTestUser({ id: 'oauth/google/vercel-credential-owner' }); + const organizationOwner = await insertTestUser(); + const organization = await createTestOrganization( + 'Vercel Credential Cleanup Org', + organizationOwner.id, + 0 + ); + await db.insert(organization_memberships).values({ + organization_id: organization.id, + kilo_user_id: user.id, + role: 'member', + }); + + const tokenEncrypted = { + scheme: 'byoc-vercel-credential-rsa-aes-256-gcm', + version: 1, + keyId: 'test-key', + ciphertext: { + encryptedData: 'encrypted-data', + encryptedDEK: 'encrypted-dek', + algorithm: 'rsa-aes-256-gcm', + version: 1, + }, + } satisfies VercelComputeCredentialEnvelope; + const personalCredentialId = randomUUID(); + const organizationCredentialId = randomUUID(); + await db.insert(organization_vercel_compute_credentials).values([ + { + id: personalCredentialId, + organization_id: null, + user_id: user.id, + token_encrypted: tokenEncrypted, + team_id: 'team_personal', + project_id: 'prj_personal', + }, + { + id: organizationCredentialId, + organization_id: organization.id, + user_id: null, + token_encrypted: tokenEncrypted, + team_id: 'team_organization', + project_id: 'prj_organization', + }, + ]); + + await db.transaction(tx => anonymizeCloudUserData(tx, user.id)); + + expect( + await db + .select() + .from(organization_vercel_compute_credentials) + .where(eq(organization_vercel_compute_credentials.id, personalCredentialId)) + ).toHaveLength(0); + expect( + await db + .select() + .from(organization_vercel_compute_credentials) + .where(eq(organization_vercel_compute_credentials.id, organizationCredentialId)) + ).toEqual([ + expect.objectContaining({ + id: organizationCredentialId, + organization_id: organization.id, + user_id: null, + token_encrypted: tokenEncrypted, + }), + ]); + }); }); describe('softDeleteUser', () => { diff --git a/apps/web/src/lib/user/index.ts b/apps/web/src/lib/user/index.ts index b73c05731c5..2ac5ab1eb1e 100644 --- a/apps/web/src/lib/user/index.ts +++ b/apps/web/src/lib/user/index.ts @@ -36,6 +36,7 @@ import { organization_groups, organization_group_memberships, organization_group_policy_settings, + organization_vercel_compute_credentials, organization_user_limits, organization_user_usage, organization_invitations, @@ -991,6 +992,8 @@ export async function assertUserCanBeSoftDeleted(userId: string): Promise * - organization_membership_removals (tombstones deleted; removed_by anonymized) * - organization_invitations (sent by user + addressed to user's email) * - organization_user_limits/usage + * - personal organization_vercel_compute_credentials rows (organization-owned + * credentials are retained) * - organization_audit_logs (actor PII nulled) * - kiloclaw_admin_audit_logs (actor PII nulled, target_user_id anonymized) * - admin_action_audit_logs (actor PII nulled, user target_id anonymized) @@ -1301,6 +1304,10 @@ export async function anonymizeCloudUserData( // User-owned resources (these would have been CASCADE-deleted if we // deleted the user row, but since we keep it, we delete them explicitly) + await tx + .delete(organization_vercel_compute_credentials) + .where(eq(organization_vercel_compute_credentials.user_id, userId)); + // cloud_agent_webhook_triggers has RESTRICT FK on agent_environment_profiles, // so delete triggers before profiles await tx diff --git a/apps/web/src/lib/vercel-client.test.ts b/apps/web/src/lib/vercel-client.test.ts new file mode 100644 index 00000000000..cac1c7b2c92 --- /dev/null +++ b/apps/web/src/lib/vercel-client.test.ts @@ -0,0 +1,653 @@ +import { + discoverVercelProjects, + discoverVercelTeams, + validateVercelSelection, + VercelApiError, +} from './vercel-client'; + +const TOKEN = 'vercel-test-token-do-not-expose'; +const PROVIDER_DETAIL = 'private-provider-response-detail'; +const TEAM = { id: 'team_one', slug: 'first-team', name: 'First Team' }; +const PROJECT = { id: 'prj_one', name: 'first-project', accountId: TEAM.id }; + +let fetchMock: jest.SpiedFunction; + +beforeEach(() => { + fetchMock = jest.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('Unexpected request')); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +function teamsPage(teams: unknown[], next: number | null = null) { + return { teams, pagination: { count: teams.length, next, prev: null } }; +} + +function projectsPage(projects: unknown[], next: number | string | null = null) { + return { + projects, + pagination: { + count: projects.length, + next, + ...(typeof next === 'number' ? { prev: null } : {}), + }, + }; +} + +async function expectSafeError(result: Promise, code: VercelApiError['code']) { + const error: unknown = await result.catch((error: unknown) => error); + expect(error).toBeInstanceOf(VercelApiError); + if (!(error instanceof VercelApiError)) throw new Error('Expected a VercelApiError'); + expect(error.code).toBe(code); + expect(error.message).toBe(new VercelApiError(code).message); + expect(error).not.toHaveProperty('cause'); + expect(Object.keys(error).sort()).toEqual(['code', 'name']); + const serialized = JSON.stringify(error, Object.getOwnPropertyNames(error)); + expect(serialized).not.toContain(TOKEN); + expect(serialized).not.toContain(PROVIDER_DETAIL); +} + +describe('discoverVercelTeams', () => { + it('returns only safe metadata, uses the slug for an unnamed team, and protects the request', async () => { + fetchMock.mockResolvedValueOnce( + Response.json({ + ...teamsPage([ + { ...TEAM, token: TOKEN, billing: { secret: PROVIDER_DETAIL } }, + { id: 'team_two', slug: 'second-team', name: null }, + ]), + token: TOKEN, + provider: PROVIDER_DETAIL, + }) + ); + + const result = await discoverVercelTeams(TOKEN); + + expect(result).toEqual([TEAM, { id: 'team_two', slug: 'second-team', name: 'second-team' }]); + expect(JSON.stringify(result)).not.toContain(TOKEN); + expect(JSON.stringify(result)).not.toContain(PROVIDER_DETAIL); + expect(fetchMock).toHaveBeenCalledWith('https://api.vercel.com/v2/teams?limit=100', { + method: 'GET', + headers: { Accept: 'application/json', Authorization: `Bearer ${TOKEN}` }, + cache: 'no-store', + redirect: 'error', + signal: expect.any(AbortSignal), + }); + }); + + it('follows timestamp pagination, including zero, without resetting the operation deadline', async () => { + const timeout = jest.spyOn(AbortSignal, 'timeout'); + const secondTeam = { id: 'team_two', slug: 'second-team', name: 'Second Team' }; + fetchMock + .mockResolvedValueOnce(Response.json(teamsPage([TEAM], 0))) + .mockResolvedValueOnce(Response.json(teamsPage([secondTeam]))); + + await expect(discoverVercelTeams(TOKEN)).resolves.toEqual([TEAM, secondTeam]); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'https://api.vercel.com/v2/teams?limit=100&until=0', + expect.anything() + ); + expect(timeout).toHaveBeenCalledTimes(1); + expect(timeout).toHaveBeenCalledWith(10_000); + expect(fetchMock.mock.calls[0]?.[1]?.signal).toBe(fetchMock.mock.calls[1]?.[1]?.signal); + }); + + it('returns an empty list without falling back to a personal account', async () => { + fetchMock + .mockResolvedValueOnce(Response.json(teamsPage([]))) + .mockResolvedValueOnce(Response.json(projectsPage([]))); + + await expect(discoverVercelTeams(TOKEN)).resolves.toEqual([]); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it('finishes pagination before excluding teams the token has no readable access to', async () => { + fetchMock + .mockResolvedValueOnce( + Response.json(teamsPage([{ ...TEAM, limited: true, limitedBy: ['scope'] }], 100)) + ) + .mockResolvedValueOnce( + Response.json(teamsPage([{ id: 'team_two', slug: 'second-team', name: null }])) + ); + + await expect(discoverVercelTeams(TOKEN)).resolves.toEqual([ + { id: 'team_two', slug: 'second-team', name: 'second-team' }, + ]); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it('returns no readable teams for a token with only limited team metadata', async () => { + fetchMock + .mockResolvedValueOnce( + Response.json(teamsPage([{ ...TEAM, limited: true, limitedBy: ['scope'] }])) + ) + .mockResolvedValueOnce(Response.json(projectsPage([]))); + + await expect(discoverVercelTeams(TOKEN)).resolves.toEqual([]); + }); + + it('infers the owning team from a project-scoped token when team access is denied', async () => { + fetchMock + .mockResolvedValueOnce(Response.json({ error: TOKEN }, { status: 403 })) + .mockResolvedValueOnce(Response.json([{ ...PROJECT, slug: 'project-slug' }])); + + await expect(discoverVercelTeams(TOKEN)).resolves.toEqual([ + { id: TEAM.id, slug: TEAM.id, name: 'Project-scoped token', scope: 'project' }, + ]); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'https://api.vercel.com/v10/projects?limit=100', + expect.objectContaining({ cache: 'no-store', redirect: 'error' }) + ); + }); + + it.each([ + ['a null response', null], + ['an unpaginated array', [TEAM]], + ['a missing team list', { pagination: { count: 0, next: null, prev: null } }], + ['a missing ID', teamsPage([{ slug: TEAM.slug, name: TEAM.name }])], + ['an empty ID', teamsPage([{ ...TEAM, id: '' }])], + ['a malformed name', teamsPage([{ ...TEAM, name: { secret: TOKEN } }])], + ['a missing name', teamsPage([{ id: TEAM.id, slug: TEAM.slug }])], + ['a malformed slug', teamsPage([{ ...TEAM, slug: null }])], + ['a malformed limited flag', teamsPage([{ ...TEAM, limited: 'true' }])], + ['missing pagination', { teams: [TEAM] }], + ['a missing next cursor', { teams: [TEAM], pagination: { count: 1, prev: null } }], + ['a missing previous cursor', { teams: [TEAM], pagination: { count: 1, next: null } }], + ['a string cursor', { teams: [TEAM], pagination: { count: 1, next: TOKEN, prev: null } }], + ['a negative cursor', teamsPage([TEAM], -1)], + ['a fractional cursor', teamsPage([TEAM], 0.5)], + [ + 'a malformed previous cursor', + { teams: [TEAM], pagination: { count: 1, next: null, prev: TOKEN } }, + ], + ['a string count', { teams: [TEAM], pagination: { count: '1', next: null, prev: null } }], + ['a negative count', { teams: [TEAM], pagination: { count: -1, next: null, prev: null } }], + ['an inconsistent count', { teams: [TEAM], pagination: { count: 2, next: null, prev: null } }], + ])('rejects %s without exposing provider data', async (_name, body) => { + fetchMock.mockResolvedValueOnce(Response.json(body)); + + await expectSafeError(discoverVercelTeams(TOKEN), 'BAD_GATEWAY'); + }); + + it('does not return a partial list when a later page fails', async () => { + fetchMock + .mockResolvedValueOnce(Response.json(teamsPage([TEAM], 100))) + .mockResolvedValueOnce(Response.json({ error: TOKEN }, { status: 429 })); + + await expectSafeError(discoverVercelTeams(TOKEN), 'TOO_MANY_REQUESTS'); + }); + + it('rejects cyclic pagination instead of returning a partial or repeated list', async () => { + fetchMock + .mockResolvedValueOnce(Response.json(teamsPage([TEAM], 100))) + .mockResolvedValueOnce(Response.json(teamsPage([TEAM], 50))) + .mockResolvedValueOnce(Response.json(teamsPage([TEAM], 100))); + + await expectSafeError(discoverVercelTeams(TOKEN), 'BAD_GATEWAY'); + expect(fetchMock).toHaveBeenCalledTimes(3); + }); + + it('bounds page count even when every page is empty', async () => { + let cursor = 100; + fetchMock.mockImplementation(async () => Response.json(teamsPage([], cursor--))); + + await expectSafeError(discoverVercelTeams(TOKEN), 'BAD_REQUEST'); + expect(fetchMock).toHaveBeenCalledTimes(20); + }); + + it('rejects a list with undiscovered results beyond the result limit', async () => { + let page = 0; + fetchMock.mockImplementation(async () => { + page++; + return Response.json( + teamsPage( + Array.from({ length: 100 }, (_, index) => ({ ...TEAM, id: `team_${page}_${index}` })), + 100 - page + ) + ); + }); + + await expectSafeError(discoverVercelTeams(TOKEN), 'BAD_REQUEST'); + expect(fetchMock).toHaveBeenCalledTimes(10); + }); + + it('accepts exactly the result limit when the final page proves the list is complete', async () => { + let page = 0; + fetchMock.mockImplementation(async () => { + page++; + return Response.json( + teamsPage( + Array.from({ length: 100 }, (_, index) => ({ ...TEAM, id: `team_${page}_${index}` })), + page === 10 ? null : 100 - page + ) + ); + }); + + await expect(discoverVercelTeams(TOKEN)).resolves.toHaveLength(1_000); + expect(fetchMock).toHaveBeenCalledTimes(10); + }); + + it('rejects an oversized final page instead of treating it as a complete supported list', async () => { + fetchMock.mockResolvedValueOnce( + Response.json( + teamsPage(Array.from({ length: 1_001 }, (_, index) => ({ ...TEAM, id: `team_${index}` }))) + ) + ); + + await expectSafeError(discoverVercelTeams(TOKEN), 'BAD_REQUEST'); + }); +}); + +describe('discoverVercelProjects', () => { + it('checks team access and normalizes accountId and name using only safe fields', async () => { + fetchMock.mockResolvedValueOnce(Response.json(TEAM)).mockResolvedValueOnce( + Response.json( + projectsPage([ + { + ...PROJECT, + slug: 'not-the-project-slug', + token: TOKEN, + env: [{ value: PROVIDER_DETAIL }], + latestDeployments: [{ secret: TOKEN }], + }, + ]) + ) + ); + + const result = await discoverVercelProjects(TOKEN, TEAM.id); + + expect(result).toEqual([ + { id: PROJECT.id, name: PROJECT.name, slug: PROJECT.name, teamId: TEAM.id }, + ]); + expect(JSON.stringify(result)).not.toContain(TOKEN); + expect(JSON.stringify(result)).not.toContain(PROVIDER_DETAIL); + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + `https://api.vercel.com/v2/teams/${TEAM.id}`, + expect.anything() + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + `https://api.vercel.com/v10/projects?teamId=${TEAM.id}&limit=100`, + expect.objectContaining({ cache: 'no-store', redirect: 'error' }) + ); + expect(fetchMock.mock.calls[0]?.[1]?.signal).toBe(fetchMock.mock.calls[1]?.[1]?.signal); + }); + + it.each([1700000000000, 'JBSWY3DPEHPK3PXP'])( + 'follows the project cursor %s with from and teamId', + async cursor => { + fetchMock + .mockResolvedValueOnce(Response.json(TEAM)) + .mockResolvedValueOnce(Response.json(projectsPage([PROJECT], cursor))) + .mockResolvedValueOnce( + Response.json(projectsPage([{ ...PROJECT, id: 'prj_two', name: 'second-project' }])) + ); + + await expect(discoverVercelProjects(TOKEN, TEAM.id)).resolves.toEqual([ + { id: PROJECT.id, name: PROJECT.name, slug: PROJECT.name, teamId: TEAM.id }, + { id: 'prj_two', name: 'second-project', slug: 'second-project', teamId: TEAM.id }, + ]); + expect(fetchMock).toHaveBeenNthCalledWith( + 3, + `https://api.vercel.com/v10/projects?teamId=${TEAM.id}&limit=100&from=${cursor}`, + expect.anything() + ); + } + ); + + it('continues empty pages and treats continuation cursors as a single query value', async () => { + const cursor = 'opaque/value+with=padding&teamId=team_other'; + fetchMock + .mockResolvedValueOnce(Response.json(TEAM)) + .mockResolvedValueOnce(Response.json(projectsPage([], cursor))) + .mockResolvedValueOnce(Response.json(projectsPage([PROJECT]))); + + await expect(discoverVercelProjects(TOKEN, TEAM.id)).resolves.toHaveLength(1); + const nextUrl = new URL(String(fetchMock.mock.calls[2]?.[0])); + expect(nextUrl.origin).toBe('https://api.vercel.com'); + expect(nextUrl.searchParams.getAll('teamId')).toEqual([TEAM.id]); + expect(nextUrl.searchParams.get('from')).toBe(cursor); + }); + + it('returns an empty project list for a readable team', async () => { + fetchMock + .mockResolvedValueOnce(Response.json(TEAM)) + .mockResolvedValueOnce(Response.json(projectsPage([]))); + + await expect(discoverVercelProjects(TOKEN, TEAM.id)).resolves.toEqual([]); + }); + + it('discovers projects with a project-only token without team scope parameters', async () => { + fetchMock + .mockResolvedValueOnce(Response.json({ error: TOKEN }, { status: 403 })) + .mockResolvedValueOnce(Response.json([{ ...PROJECT, slug: 'project-slug' }])); + + await expect(discoverVercelProjects(TOKEN, TEAM.id)).resolves.toEqual([ + { id: PROJECT.id, name: PROJECT.name, slug: PROJECT.name, teamId: TEAM.id }, + ]); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + `https://api.vercel.com/v10/projects?limit=100`, + expect.objectContaining({ cache: 'no-store', redirect: 'error' }) + ); + }); + + it.each([ + ['a null response', null], + ['a malformed project list', { ...projectsPage([]), projects: {} }], + ['a missing account ID', projectsPage([{ id: PROJECT.id, name: PROJECT.name }])], + ['an empty project ID', projectsPage([{ ...PROJECT, id: '' }])], + ['an empty project name', projectsPage([{ ...PROJECT, name: '' }])], + ['a malformed project name', projectsPage([{ ...PROJECT, name: { secret: TOKEN } }])], + ['missing pagination', { projects: [PROJECT] }], + ['a missing next cursor', { projects: [PROJECT], pagination: { count: 1 } }], + ['a missing previous timestamp', { projects: [PROJECT], pagination: { count: 1, next: 100 } }], + ['an empty continuation cursor', projectsPage([PROJECT], '')], + [ + 'a malformed continuation cursor', + { projects: [PROJECT], pagination: { count: 1, next: { secret: TOKEN } } }, + ], + [ + 'a malformed previous cursor', + { projects: [PROJECT], pagination: { count: 1, next: null, prev: TOKEN } }, + ], + ['an inconsistent count', { projects: [PROJECT], pagination: { count: 2, next: null } }], + ])('rejects %s instead of returning an incomplete selection', async (_name, body) => { + fetchMock.mockResolvedValueOnce(Response.json(TEAM)).mockResolvedValueOnce(Response.json(body)); + + await expectSafeError(discoverVercelProjects(TOKEN, TEAM.id), 'BAD_GATEWAY'); + }); + + it.each(['team_other', 'user_personal'])( + 'rejects a project owned by %s rather than filtering it out', + async accountId => { + fetchMock + .mockResolvedValueOnce(Response.json(TEAM)) + .mockResolvedValueOnce( + Response.json(projectsPage([PROJECT, { ...PROJECT, id: 'prj_other', accountId }])) + ); + + await expectSafeError(discoverVercelProjects(TOKEN, TEAM.id), 'FORBIDDEN'); + } + ); + + it('rejects repeated project continuation cursors', async () => { + fetchMock + .mockResolvedValueOnce(Response.json(TEAM)) + .mockResolvedValueOnce(Response.json(projectsPage([PROJECT], 'JBSWY3DPEHPK3PXP'))) + .mockResolvedValueOnce(Response.json(projectsPage([PROJECT], 'JBSWY3DPEHPK3PXP'))); + + await expectSafeError(discoverVercelProjects(TOKEN, TEAM.id), 'BAD_GATEWAY'); + expect(fetchMock).toHaveBeenCalledTimes(3); + }); +}); + +describe('Vercel request failures', () => { + it.each([ + [401, 'UNAUTHORIZED'], + [403, 'FORBIDDEN'], + [404, 'NOT_FOUND'], + [410, 'NOT_FOUND'], + [429, 'TOO_MANY_REQUESTS'], + [408, 'SERVICE_UNAVAILABLE'], + [500, 'SERVICE_UNAVAILABLE'], + [502, 'SERVICE_UNAVAILABLE'], + [503, 'SERVICE_UNAVAILABLE'], + [504, 'SERVICE_UNAVAILABLE'], + [400, 'BAD_GATEWAY'], + [422, 'BAD_GATEWAY'], + [302, 'BAD_GATEWAY'], + ] as const)( + 'maps HTTP %s to %s without reading or retaining the provider error', + async (status, code) => { + const response = Response.json( + { error: { message: `${TOKEN} ${PROVIDER_DETAIL}`, cause: { token: TOKEN } } }, + { status, statusText: PROVIDER_DETAIL } + ); + const readBody = jest.spyOn(response, 'json'); + fetchMock.mockResolvedValueOnce(response); + if (status === 403) { + fetchMock.mockResolvedValueOnce( + Response.json({ error: `${TOKEN} ${PROVIDER_DETAIL}` }, { status: 403 }) + ); + } + + await expectSafeError(discoverVercelTeams(TOKEN), code); + expect(readBody).not.toHaveBeenCalled(); + } + ); + + it('sanitizes network errors and their causes', async () => { + fetchMock.mockRejectedValueOnce( + new Error(`${TOKEN} ${PROVIDER_DETAIL}`, { cause: { authorization: TOKEN } }) + ); + + await expectSafeError(discoverVercelTeams(TOKEN), 'SERVICE_UNAVAILABLE'); + }); + + it('sanitizes JSON syntax errors containing provider body text', async () => { + fetchMock.mockResolvedValueOnce(new Response(`${TOKEN} ${PROVIDER_DETAIL}`)); + + await expectSafeError(discoverVercelTeams(TOKEN), 'BAD_GATEWAY'); + }); + + it('sanitizes rejected body reads and their causes', async () => { + const response = Response.json(teamsPage([TEAM])); + jest + .spyOn(response, 'json') + .mockRejectedValueOnce(new Error(`${TOKEN} ${PROVIDER_DETAIL}`, { cause: { token: TOKEN } })); + fetchMock.mockResolvedValueOnce(response); + + await expectSafeError(discoverVercelTeams(TOKEN), 'BAD_GATEWAY'); + }); + + it('sanitizes fetch timeouts without waiting for a real timeout', async () => { + const controller = new AbortController(); + jest.spyOn(AbortSignal, 'timeout').mockReturnValueOnce(controller.signal); + fetchMock.mockImplementationOnce( + (_url, init) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener( + 'abort', + () => reject(new Error(`${TOKEN} ${PROVIDER_DETAIL}`)), + { once: true } + ); + }) + ); + + const result = discoverVercelTeams(TOKEN); + controller.abort(); + + await expectSafeError(result, 'SERVICE_UNAVAILABLE'); + }); + + it('keeps the timeout active while reading the response body', async () => { + const controller = new AbortController(); + jest.spyOn(AbortSignal, 'timeout').mockReturnValueOnce(controller.signal); + const response = Response.json(teamsPage([TEAM])); + jest.spyOn(response, 'json').mockImplementationOnce(async () => { + controller.abort(); + throw new Error(`${TOKEN} ${PROVIDER_DETAIL}`); + }); + fetchMock.mockResolvedValueOnce(response); + + await expectSafeError(discoverVercelTeams(TOKEN), 'SERVICE_UNAVAILABLE'); + }); + + it('rejects an unexpectedly redirected response', async () => { + const response = Response.json(teamsPage([TEAM])); + Object.defineProperty(response, 'redirected', { value: true }); + fetchMock.mockResolvedValueOnce(response); + + await expectSafeError(discoverVercelTeams(TOKEN), 'BAD_GATEWAY'); + }); +}); + +describe('validateVercelSelection', () => { + it('waits for team access before inspecting the exact team-scoped project and returns no provider data', async () => { + const teamResponse = Promise.withResolvers(); + const timeout = jest.spyOn(AbortSignal, 'timeout'); + fetchMock + .mockReturnValueOnce(teamResponse.promise) + .mockResolvedValueOnce( + Response.json({ ...PROJECT, token: TOKEN, env: [{ value: PROVIDER_DETAIL }] }) + ); + + const result = validateVercelSelection(TOKEN, TEAM.id, PROJECT.id); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + `https://api.vercel.com/v2/teams/${TEAM.id}`, + expect.anything() + ); + teamResponse.resolve(Response.json(TEAM)); + + await expect(result).resolves.toEqual({ + teamSlug: TEAM.slug, + projectSlug: PROJECT.name, + tokenScope: 'team', + }); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + `https://api.vercel.com/v9/projects/${PROJECT.id}?teamId=${TEAM.id}`, + { + method: 'GET', + headers: { Accept: 'application/json', Authorization: `Bearer ${TOKEN}` }, + cache: 'no-store', + redirect: 'error', + signal: expect.any(AbortSignal), + } + ); + expect(timeout).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0]?.[1]?.signal).toBe(fetchMock.mock.calls[1]?.[1]?.signal); + }); + + it('validates a project-scoped token with the inferred project and no team request', async () => { + fetchMock + .mockResolvedValueOnce(Response.json({ error: TOKEN }, { status: 403 })) + .mockResolvedValueOnce(Response.json({ ...PROJECT, slug: 'project-slug' })); + + await expect(validateVercelSelection(TOKEN, TEAM.id, PROJECT.id)).resolves.toEqual({ + teamSlug: null, + projectSlug: PROJECT.name, + tokenScope: 'project', + }); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + `https://api.vercel.com/v9/projects/${PROJECT.id}`, + expect.objectContaining({ + headers: { Accept: 'application/json', Authorization: `Bearer ${TOKEN}` }, + }) + ); + }); + + it.each([ + [401, 'UNAUTHORIZED'], + [404, 'NOT_FOUND'], + ] as const)( + 'rejects unavailable team access (%s) without trying the project', + async (status, code) => { + fetchMock.mockResolvedValueOnce(Response.json({ error: TOKEN }, { status })); + + await expectSafeError(validateVercelSelection(TOKEN, TEAM.id, PROJECT.id), code); + expect(fetchMock).toHaveBeenCalledTimes(1); + } + ); + + it('rejects a personally owned project even when its account ID matches the submitted team ID', async () => { + const accountId = 'user_personal'; + fetchMock + .mockResolvedValueOnce(Response.json({ error: TOKEN }, { status: 403 })) + .mockResolvedValueOnce(Response.json({ ...PROJECT, accountId })); + + await expectSafeError(validateVercelSelection(TOKEN, accountId, PROJECT.id), 'FORBIDDEN'); + }); + + it('maps denied project-scope validation to the provider error', async () => { + fetchMock + .mockResolvedValueOnce(Response.json({ error: TOKEN }, { status: 403 })) + .mockResolvedValueOnce(Response.json({ error: TOKEN }, { status: 403 })); + + await expectSafeError(validateVercelSelection(TOKEN, TEAM.id, PROJECT.id), 'FORBIDDEN'); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it.each([ + ['a different team', { ...TEAM, id: 'team_other' }, 'FORBIDDEN'], + ['a personal account', { ...TEAM, id: 'user_personal' }, 'FORBIDDEN'], + ['limited team access', { ...TEAM, limited: true, limitedBy: ['scope'] }, 'FORBIDDEN'], + ['a malformed team', { ...TEAM, id: { token: TOKEN } }, 'BAD_GATEWAY'], + ] as const)('rejects %s before inspecting the project', async (_name, body, code) => { + fetchMock.mockResolvedValueOnce(Response.json(body)); + + await expectSafeError(validateVercelSelection(TOKEN, TEAM.id, PROJECT.id), code); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it('requires the submitted team ID to match rather than accepting a team slug', async () => { + fetchMock.mockResolvedValueOnce(Response.json(TEAM)); + + await expectSafeError(validateVercelSelection(TOKEN, TEAM.slug, PROJECT.id), 'FORBIDDEN'); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it.each([ + ['a different project', { ...PROJECT, id: 'prj_other' }, 'FORBIDDEN'], + ['a different owning team', { ...PROJECT, accountId: 'team_other' }, 'FORBIDDEN'], + ['a personally owned project', { ...PROJECT, accountId: 'user_personal' }, 'FORBIDDEN'], + ['a missing account ID', { id: PROJECT.id, name: PROJECT.name }, 'BAD_GATEWAY'], + ['a malformed project', { ...PROJECT, id: { token: TOKEN } }, 'BAD_GATEWAY'], + ] as const)('rejects %s during final validation', async (_name, body, code) => { + fetchMock.mockResolvedValueOnce(Response.json(TEAM)).mockResolvedValueOnce(Response.json(body)); + + await expectSafeError(validateVercelSelection(TOKEN, TEAM.id, PROJECT.id), code); + }); + + it('requires the submitted project ID to match rather than accepting the project name', async () => { + fetchMock + .mockResolvedValueOnce(Response.json(TEAM)) + .mockResolvedValueOnce(Response.json(PROJECT)); + + await expectSafeError(validateVercelSelection(TOKEN, TEAM.id, PROJECT.name), 'FORBIDDEN'); + }); + + it('uses a changed token and rechecks team access after successful discovery', async () => { + fetchMock + .mockResolvedValueOnce(Response.json(teamsPage([TEAM]))) + .mockResolvedValueOnce(Response.json({ error: TOKEN }, { status: 401 })); + await discoverVercelTeams(TOKEN); + + await expectSafeError( + validateVercelSelection('changed-token', TEAM.id, PROJECT.id), + 'UNAUTHORIZED' + ); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock).toHaveBeenLastCalledWith( + `https://api.vercel.com/v2/teams/${TEAM.id}`, + expect.objectContaining({ + headers: { Accept: 'application/json', Authorization: 'Bearer changed-token' }, + }) + ); + }); + + it.each([ + [403, 'FORBIDDEN'], + [404, 'NOT_FOUND'], + ] as const)( + 'rechecks a project whose access changed after discovery (%s)', + async (status, code) => { + fetchMock + .mockResolvedValueOnce(Response.json(TEAM)) + .mockResolvedValueOnce(Response.json(projectsPage([PROJECT]))) + .mockResolvedValueOnce(Response.json(TEAM)) + .mockResolvedValueOnce(Response.json({ error: TOKEN }, { status })); + await discoverVercelProjects(TOKEN, TEAM.id); + + await expectSafeError(validateVercelSelection(TOKEN, TEAM.id, PROJECT.id), code); + expect(fetchMock).toHaveBeenCalledTimes(4); + } + ); +}); diff --git a/apps/web/src/lib/vercel-client.ts b/apps/web/src/lib/vercel-client.ts new file mode 100644 index 00000000000..3777af2de1f --- /dev/null +++ b/apps/web/src/lib/vercel-client.ts @@ -0,0 +1,315 @@ +import 'server-only'; + +import { z } from 'zod'; + +const VERCEL_API_URL = 'https://api.vercel.com'; +const OPERATION_TIMEOUT_MS = 10_000; +const PAGE_SIZE = 100; +const MAX_PAGES = 20; +const MAX_RESULTS = 1_000; + +const ERROR_MESSAGES = { + UNAUTHORIZED: 'The Vercel token is invalid or expired.', + FORBIDDEN: 'The Vercel token cannot access the selected team or project.', + NOT_FOUND: 'The selected Vercel team or project was not found.', + TOO_MANY_REQUESTS: 'Vercel is rate limiting requests. Try again later.', + SERVICE_UNAVAILABLE: 'Vercel is temporarily unavailable. Try again later.', + BAD_GATEWAY: 'Vercel returned an invalid response.', + BAD_REQUEST: 'Vercel returned too many teams or projects to discover safely.', +} as const; + +export class VercelApiError extends Error { + constructor(readonly code: keyof typeof ERROR_MESSAGES) { + super(ERROR_MESSAGES[code]); + this.name = 'VercelApiError'; + } +} + +export type VercelTeam = { + id: string; + slug: string; + name: string; + scope?: 'project'; +}; + +export type VercelProject = { + id: string; + name: string; + slug: string; + teamId: string; +}; + +export type VercelSelectionValidation = { + teamSlug: string | null; + projectSlug: string; + tokenScope: 'team' | 'project'; +}; + +const TeamSchema = z.object({ + id: z.string().min(1), + slug: z.string().min(1), + name: z.string().nullable(), + limited: z.boolean().optional(), +}); + +const ProjectSchema = z.object({ + id: z.string().min(1), + name: z.string().min(1), + accountId: z.string().min(1), +}); + +const TimestampSchema = z.number().int().nonnegative(); +const PaginationSchema = z.object({ + count: z.number().int().nonnegative(), + next: TimestampSchema.nullable(), + prev: TimestampSchema.nullable(), +}); + +const TeamsPageSchema = z + .object({ + teams: z.array(TeamSchema), + pagination: PaginationSchema, + }) + .transform(({ teams, pagination }) => ({ items: teams, pagination })); + +const ProjectsPageSchema = z.union([ + z + .object({ + projects: z.array(ProjectSchema), + pagination: z.union([ + PaginationSchema, + PaginationSchema.extend({ + next: z.string().min(1).nullable(), + prev: TimestampSchema.nullable().optional(), + }), + ]), + }) + .transform(({ projects, pagination }) => ({ items: projects, pagination })), + z.array(ProjectSchema).transform(projects => ({ + items: projects, + pagination: { count: projects.length, next: null }, + })), +]); + +type VercelPage = { + items: T[]; + pagination: { count: number; next: number | string | null }; +}; + +async function requestVercel( + token: string, + url: URL, + schema: z.ZodType, + signal: AbortSignal +): Promise { + let response: Response; + try { + response = await fetch(url.toString(), { + method: 'GET', + headers: { Accept: 'application/json', Authorization: `Bearer ${token}` }, + cache: 'no-store', + redirect: 'error', + signal, + }); + } catch { + throw new VercelApiError('SERVICE_UNAVAILABLE'); + } + + if (response.redirected) throw new VercelApiError('BAD_GATEWAY'); + if (!response.ok) { + switch (response.status) { + case 401: + throw new VercelApiError('UNAUTHORIZED'); + case 403: + throw new VercelApiError('FORBIDDEN'); + case 404: + case 410: + throw new VercelApiError('NOT_FOUND'); + case 429: + throw new VercelApiError('TOO_MANY_REQUESTS'); + default: + throw new VercelApiError( + response.status >= 500 || response.status === 408 ? 'SERVICE_UNAVAILABLE' : 'BAD_GATEWAY' + ); + } + } + + try { + return schema.parse(await response.json()); + } catch { + throw new VercelApiError(signal.aborted ? 'SERVICE_UNAVAILABLE' : 'BAD_GATEWAY'); + } +} + +async function listVercelResources( + token: string, + url: URL, + schema: z.ZodType>, + cursorParameter: 'until' | 'from', + signal: AbortSignal +): Promise { + const results: T[] = []; + const seenCursors = new Set(); + url.searchParams.set('limit', String(PAGE_SIZE)); + + for (let page = 0; page < MAX_PAGES; page++) { + const { items, pagination } = await requestVercel(token, url, schema, signal); + if (pagination.count !== items.length) throw new VercelApiError('BAD_GATEWAY'); + if (results.length + items.length > MAX_RESULTS) throw new VercelApiError('BAD_REQUEST'); + results.push(...items); + if (pagination.next === null) return results; + if (results.length === MAX_RESULTS) throw new VercelApiError('BAD_REQUEST'); + + const next = String(pagination.next); + if (seenCursors.has(next)) throw new VercelApiError('BAD_GATEWAY'); + seenCursors.add(next); + url.searchParams.set(cursorParameter, next); + } + + throw new VercelApiError('BAD_REQUEST'); +} + +async function inspectVercelTeam( + token: string, + teamId: string, + signal: AbortSignal +): Promise> { + let team: z.infer; + try { + team = await requestVercel( + token, + new URL(`/v2/teams/${encodeURIComponent(teamId)}`, VERCEL_API_URL), + TeamSchema, + signal + ); + } catch (error) { + if (error instanceof VercelApiError && error.code === 'FORBIDDEN') { + throw new VercelTeamScopeUnavailableError(); + } + throw error; + } + if (team.id !== teamId || team.limited) throw new VercelApiError('FORBIDDEN'); + return team; +} + +class VercelTeamScopeUnavailableError extends Error { + constructor() { + super('Vercel team scope is unavailable'); + this.name = 'VercelTeamScopeUnavailableError'; + } +} + +function canFallbackToProjectScope(error: unknown): boolean { + return error instanceof VercelTeamScopeUnavailableError; +} + +async function listVercelProjects( + token: string, + teamId: string | undefined, + signal: AbortSignal +): Promise[]> { + const url = new URL('/v10/projects', VERCEL_API_URL); + if (teamId !== undefined) url.searchParams.set('teamId', teamId); + return listVercelResources(token, url, ProjectsPageSchema, 'from', signal); +} + +async function discoverProjectScopedTeams( + token: string, + signal: AbortSignal +): Promise { + const projects = await listVercelProjects(token, undefined, signal); + const teamIds = [...new Set(projects.map(project => project.accountId))].filter(teamId => + teamId.startsWith('team_') + ); + + return teamIds.map(teamId => ({ + id: teamId, + // Project-scoped tokens cannot read team metadata. Keep the account ID in + // the safe display field so the administrator can distinguish targets. + slug: teamId, + name: 'Project-scoped token', + scope: 'project', + })); +} + +export async function discoverVercelTeams(token: string): Promise { + const signal = AbortSignal.timeout(OPERATION_TIMEOUT_MS); + try { + const teams = await listVercelResources( + token, + new URL('/v2/teams', VERCEL_API_URL), + TeamsPageSchema, + 'until', + signal + ); + const readableTeams = teams + .filter(team => !team.limited) + .map(({ id, slug, name }) => ({ id, slug, name: name || slug })); + if (readableTeams.length > 0) return readableTeams; + } catch (error) { + if (!(error instanceof VercelApiError && error.code === 'FORBIDDEN')) throw error; + } + + return discoverProjectScopedTeams(token, signal); +} + +export async function discoverVercelProjects( + token: string, + teamId: string +): Promise { + const signal = AbortSignal.timeout(OPERATION_TIMEOUT_MS); + try { + await inspectVercelTeam(token, teamId, signal); + } catch (error) { + if (!canFallbackToProjectScope(error)) throw error; + const projects = await listVercelProjects(token, undefined, signal); + return projects + .filter(project => project.accountId === teamId) + .map(({ id, name }) => ({ id, name, slug: name, teamId })); + } + + const projects = await listVercelProjects(token, teamId, signal); + if (projects.some(project => project.accountId !== teamId)) { + throw new VercelApiError('FORBIDDEN'); + } + return projects.map(({ id, name }) => ({ id, name, slug: name, teamId })); +} + +export async function validateVercelSelection( + token: string, + teamId: string, + projectId: string +): Promise { + const signal = AbortSignal.timeout(OPERATION_TIMEOUT_MS); + let team: z.infer; + try { + team = await inspectVercelTeam(token, teamId, signal); + } catch (error) { + if (!canFallbackToProjectScope(error)) throw error; + const project = await inspectVercelProject(token, teamId, projectId, signal, false); + return { teamSlug: null, projectSlug: project.name, tokenScope: 'project' }; + } + + const project = await inspectVercelProject(token, teamId, projectId, signal, true); + return { teamSlug: team.slug, projectSlug: project.name, tokenScope: 'team' }; +} + +async function inspectVercelProject( + token: string, + teamId: string, + projectId: string, + signal: AbortSignal, + teamScoped: boolean +): Promise> { + const url = new URL(`/v9/projects/${encodeURIComponent(projectId)}`, VERCEL_API_URL); + if (teamScoped) url.searchParams.set('teamId', teamId); + const project = await requestVercel(token, url, ProjectSchema, signal); + if ( + project.id !== projectId || + project.accountId !== teamId || + !project.accountId.startsWith('team_') + ) { + throw new VercelApiError('FORBIDDEN'); + } + return project; +} diff --git a/apps/web/src/routers/cloud-agent-next-schemas.test.ts b/apps/web/src/routers/cloud-agent-next-schemas.test.ts index 784f6b74d15..6a556a849d6 100644 --- a/apps/web/src/routers/cloud-agent-next-schemas.test.ts +++ b/apps/web/src/routers/cloud-agent-next-schemas.test.ts @@ -889,8 +889,8 @@ describe('prepare session sandbox selection', () => { { vcpus: 4 }, { provider: { id: 'cloudflare', account: 'byoc' }, instanceType: 'single' }, { provider: { id: 'cloudflare', account: 'kilo' }, instanceType: 'small' }, - { provider: { id: 'vercel', account: 'kilo' }, instanceType: 'single' }, - { provider: { id: 'vercel', account: 'kilo' }, instanceType: 'default' }, + { provider: { id: 'vercel', account: 'byoc' }, instanceType: 'single' }, + { provider: { id: 'vercel', account: 'byoc' }, instanceType: 'default' }, { provider: { id: 'cloudflare', account: 'kilo' }, instanceType: 'devcontainer' }, ])('rejects an invalid sandbox allocation: %j', sandboxAllocation => { expect( diff --git a/apps/web/src/routers/cloud-agent-settings-router.test.ts b/apps/web/src/routers/cloud-agent-settings-router.test.ts index 671171230da..a319b7401aa 100644 --- a/apps/web/src/routers/cloud-agent-settings-router.test.ts +++ b/apps/web/src/routers/cloud-agent-settings-router.test.ts @@ -12,7 +12,7 @@ import { } from '@kilocode/db/schema'; import { and, eq, inArray, isNull, or } from 'drizzle-orm'; import { - getKiloSandboxAllocation, + getSandboxAllocation, getSandboxAllocationRequest, getSandboxAllocationProvider, SELECTABLE_SANDBOX_ALLOCATIONS, @@ -453,14 +453,14 @@ describe('cloud-agent-settings router', () => { const caller = await createCallerForUser(ownerUser.id); const result = await caller.cloudAgentSettings.get({}); const gitlabVercel = result.capabilitiesByPlatform.gitlab.options.find( - option => getKiloSandboxAllocation(option.allocation) === 'vercel-small' + option => getSandboxAllocation(option.allocation) === 'vercel-small' ); expect(gitlabVercel?.repositoryUnavailableReason).toBe( 'Vercel sandboxes support GitHub repositories only' ); expect(gitlabVercel?.unavailableReason).toBeUndefined(); const githubVercel = result.capabilitiesByPlatform.github.options.find( - option => getKiloSandboxAllocation(option.allocation) === 'vercel-small' + option => getSandboxAllocation(option.allocation) === 'vercel-small' ); expect(githubVercel?.repositoryUnavailableReason).toBeUndefined(); }); diff --git a/apps/web/src/routers/organizations/organization-cloud-agent-next-router.test.ts b/apps/web/src/routers/organizations/organization-cloud-agent-next-router.test.ts index a6d11b92aeb..7b10eb547cb 100644 --- a/apps/web/src/routers/organizations/organization-cloud-agent-next-router.test.ts +++ b/apps/web/src/routers/organizations/organization-cloud-agent-next-router.test.ts @@ -1638,7 +1638,7 @@ describe('organizationCloudAgentNextRouter.getSandboxSelectionOptions', () => { it('authorizes membership and forwards the organization with the caller token', async () => { const capabilities: SandboxSelectionCapabilities = { enabled: true, - defaultDestination: { provider: { id: 'vercel', account: 'kilo' }, instanceType: 'default' }, + defaultDestination: { provider: { id: 'vercel', account: 'byoc' }, instanceType: 'default' }, options: [ { allocation: getSandboxAllocationRequest('cloudflare-single') }, { @@ -1719,7 +1719,7 @@ describe('organizationCloudAgentNextRouter.getSandboxSelectionOptions', () => { options: [ { allocation: { - provider: { id: 'vercel', account: 'kilo' }, + provider: { id: 'vercel', account: 'byoc' }, instanceType: 'medium' as 'small', }, }, diff --git a/apps/web/src/routers/organizations/organization-router.ts b/apps/web/src/routers/organizations/organization-router.ts index 6304d6027d4..7fe2ffdfda6 100644 --- a/apps/web/src/routers/organizations/organization-router.ts +++ b/apps/web/src/routers/organizations/organization-router.ts @@ -72,6 +72,7 @@ import { createAuditLog } from '@kilocode/web-shared/lib/organizations/organizat import PostHogClient from '@kilocode/web-shared/lib/posthog'; import { organizationReviewAgentRouter } from '@/routers/organizations/organization-code-reviews-router'; import { organizationCloudAgentNextRouter } from '@/routers/organizations/organization-cloud-agent-next-router'; +import { organizationVercelComputeRouter } from '@/routers/organizations/organization-vercel-compute-router'; import { organizationSecurityAgentRouter } from '@/routers/organizations/organization-security-agent-router'; import { organizationSecurityAuditLogRouter } from '@/routers/organizations/organization-security-audit-log-router'; import { organizationAutoTopUpRouter } from '@/routers/organizations/organization-auto-top-up-router'; @@ -145,6 +146,7 @@ export const organizationsRouter = createTRPCRouter({ modes: organizationModesRouter, reviewAgent: organizationReviewAgentRouter, cloudAgentNext: organizationCloudAgentNextRouter, + vercelCompute: organizationVercelComputeRouter, securityAgent: organizationSecurityAgentRouter, securityAuditLog: organizationSecurityAuditLogRouter, autoTopUp: organizationAutoTopUpRouter, diff --git a/apps/web/src/routers/organizations/organization-vercel-compute-router.test.ts b/apps/web/src/routers/organizations/organization-vercel-compute-router.test.ts new file mode 100644 index 00000000000..99efb75df45 --- /dev/null +++ b/apps/web/src/routers/organizations/organization-vercel-compute-router.test.ts @@ -0,0 +1,780 @@ +import { NextRequest } from 'next/server'; +import { INTERNAL_API_SECRET } from '@kilocode/web-shared/lib/config.server'; +import { + GET as getInternalCredential, + PATCH as updateInternalCredential, +} from '@/app/api/internal/byoc/vercel-credentials/[credentialId]/route'; +jest.mock('@/lib/cloud-agent-next/cloud-agent-client', () => ({ + cleanupVercelSnapshotBuild: jest.fn(), + getVercelComputeEnrollment: jest.fn(), + getVercelComputeEnrollmentForUser: jest.fn(), + getVercelRuntimeIdentity: jest.fn(), + startVercelSnapshotBuild: jest.fn(), +})); + +jest.mock('@kilocode/web-shared/lib/config.server', () => { + const actual = jest.requireActual( + '@kilocode/web-shared/lib/config.server' + ); + const { generateKeyPairSync } = jest.requireActual('node:crypto'); + const { publicKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + return { ...actual, AGENT_ENV_VARS_PUBLIC_KEY: Buffer.from(publicKey).toString('base64') }; +}); + +import type * as NodeCryptoModule from 'node:crypto'; +import type * as ConfigServerModule from '@kilocode/web-shared/lib/config.server'; +import { TRPCError } from '@trpc/server'; +import { eq, or } from 'drizzle-orm'; +import { + organization_memberships, + organization_vercel_compute_credentials, + organizations, + type User, +} from '@kilocode/db/schema'; +import { + cleanupVercelSnapshotBuild, + getVercelComputeEnrollment, + getVercelComputeEnrollmentForUser, + getVercelRuntimeIdentity, + startVercelSnapshotBuild, +} from '@/lib/cloud-agent-next/cloud-agent-client'; +import { db } from '@kilocode/web-shared/lib/drizzle'; +import { createCallerFactory } from '@kilocode/web-shared/lib/trpc/init'; +import { insertTestUser } from '@kilocode/web-shared/tests/helpers/user.helper'; +import { + organizationVercelComputeRouter, + personalVercelComputeRouter, +} from './organization-vercel-compute-router'; + +const TOKEN = 'vercel-router-test-token-do-not-expose'; +const PROVIDER_DETAIL = 'private-provider-response-detail'; +const TEAM = { id: 'team_test', slug: 'test-team', name: 'Test Team' }; +const PROJECT = { id: 'prj_test', name: 'test-project', accountId: TEAM.id }; +const organizationId = crypto.randomUUID(); +const setupInput = { organizationId, token: TOKEN, teamId: TEAM.id, projectId: PROJECT.id }; +const providerProcedures = ['discoverTeams', 'discoverProjects', 'add'] as const; +const createCaller = createCallerFactory(organizationVercelComputeRouter); +const createPersonalCaller = createCallerFactory(personalVercelComputeRouter); +const mockEnrollment = jest.mocked(getVercelComputeEnrollment); +const mockPersonalEnrollment = jest.mocked(getVercelComputeEnrollmentForUser); +const mockStartBuild = jest.mocked(startVercelSnapshotBuild); +const mockCleanupBuild = jest.mocked(cleanupVercelSnapshotBuild); +const mockRuntimeIdentity = jest.mocked(getVercelRuntimeIdentity); + +let owner: User; +let organizationAdmin: User; +let billingManager: User; +let member: User; +let outsider: User; +let personalUser: User; +let fetchMock: jest.SpiedFunction; + +beforeAll(async () => { + owner = await insertTestUser(); + organizationAdmin = await insertTestUser(); + billingManager = await insertTestUser(); + member = await insertTestUser(); + outsider = await insertTestUser(); + personalUser = await insertTestUser(); + await db.insert(organizations).values({ id: organizationId, name: 'Vercel Discovery Test' }); + await db.insert(organization_memberships).values([ + { organization_id: organizationId, kilo_user_id: owner.id, role: 'owner' }, + { organization_id: organizationId, kilo_user_id: organizationAdmin.id, role: 'admin' }, + { organization_id: organizationId, kilo_user_id: billingManager.id, role: 'billing_manager' }, + { organization_id: organizationId, kilo_user_id: member.id, role: 'member' }, + ]); +}); + +beforeEach(async () => { + await db + .delete(organization_vercel_compute_credentials) + .where( + or( + eq(organization_vercel_compute_credentials.organization_id, organizationId), + eq(organization_vercel_compute_credentials.user_id, personalUser.id) + ) + ); + jest.resetAllMocks(); + mockEnrollment.mockResolvedValue(true); + mockPersonalEnrollment.mockResolvedValue(true); + mockStartBuild.mockResolvedValue(undefined); + mockCleanupBuild.mockResolvedValue(undefined); + mockRuntimeIdentity.mockRejectedValue(new Error('runtime identity unused')); + fetchMock = jest.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('Unexpected request')); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +function mockTeamDiscovery() { + fetchMock.mockResolvedValueOnce( + Response.json({ + teams: [{ ...TEAM, token: TOKEN, billing: { secret: PROVIDER_DETAIL } }], + pagination: { count: 1, next: null, prev: null }, + }) + ); +} + +function mockProjectDiscovery() { + fetchMock.mockResolvedValueOnce(Response.json(TEAM)).mockResolvedValueOnce( + Response.json({ + projects: [{ ...PROJECT, env: [{ value: TOKEN }], secret: PROVIDER_DETAIL }], + pagination: { count: 1, next: null, prev: null }, + }) + ); +} + +function mockSelectionValidation(project = PROJECT) { + fetchMock + .mockResolvedValueOnce(Response.json(TEAM)) + .mockResolvedValueOnce(Response.json(project)); +} + +function getCredential() { + return db.query.organization_vercel_compute_credentials.findFirst({ + where: eq(organization_vercel_compute_credentials.organization_id, organizationId), + }); +} + +function getPersonalCredential() { + return db.query.organization_vercel_compute_credentials.findFirst({ + where: eq(organization_vercel_compute_credentials.user_id, personalUser.id), + }); +} + +async function expectNoSetup() { + expect(await getCredential()).toBeUndefined(); + expect(mockStartBuild).not.toHaveBeenCalled(); + expect(mockCleanupBuild).not.toHaveBeenCalled(); +} + +async function expectNoPersonalSetup() { + expect(await getPersonalCredential()).toBeUndefined(); + expect(mockStartBuild).not.toHaveBeenCalled(); + expect(mockCleanupBuild).not.toHaveBeenCalled(); +} + +async function expectSanitizedError(result: Promise, code: TRPCError['code']) { + const error: unknown = await result.catch((error: unknown) => error); + expect(error).toBeInstanceOf(TRPCError); + if (!(error instanceof TRPCError)) throw new Error('Expected a TRPCError'); + expect(error.code).toBe(code); + expect(error.cause).toBeUndefined(); + const serialized = JSON.stringify(error, Object.getOwnPropertyNames(error)); + expect(serialized).not.toContain(TOKEN); + expect(serialized).not.toContain(PROVIDER_DETAIL); +} + +describe('Vercel discovery authorization', () => { + it('allows owners and organization admins to discover safe metadata without creating setup rows', async () => { + for (const user of [owner, organizationAdmin]) { + const caller = createCaller({ user }); + mockTeamDiscovery(); + await expect(caller.discoverTeams({ organizationId, token: TOKEN })).resolves.toEqual([TEAM]); + mockProjectDiscovery(); + await expect( + caller.discoverProjects({ organizationId, token: TOKEN, teamId: TEAM.id }) + ).resolves.toEqual([ + { id: PROJECT.id, name: PROJECT.name, slug: PROJECT.name, teamId: TEAM.id }, + ]); + await expect(caller.getStatus({ organizationId })).resolves.toBeNull(); + } + await expectNoSetup(); + }); + + it('accepts a project-scoped token and preserves the missing team slug', async () => { + const caller = createCaller({ user: owner }); + fetchMock + .mockResolvedValueOnce(Response.json({ error: TOKEN }, { status: 403 })) + .mockResolvedValueOnce(Response.json(PROJECT)); + + const status = await caller.add(setupInput); + const credential = await getCredential(); + + expect(status).toMatchObject({ + teamId: TEAM.id, + projectId: PROJECT.id, + teamSlug: null, + projectSlug: PROJECT.name, + }); + expect(credential).toMatchObject({ + token_scope: 'project', + team_id: TEAM.id, + project_id: PROJECT.id, + team_slug: null, + }); + expect(mockStartBuild).toHaveBeenCalledTimes(1); + }); + + it('rejects billing managers, members, and nonmembers before checking enrollment or Vercel', async () => { + for (const user of [billingManager, member, outsider]) { + const caller = createCaller({ user }); + for (const procedure of providerProcedures) { + await expectSanitizedError(caller[procedure](setupInput), 'UNAUTHORIZED'); + } + } + expect(mockEnrollment).not.toHaveBeenCalled(); + expect(fetchMock).not.toHaveBeenCalled(); + await expectNoSetup(); + }); + + it.each(providerProcedures)( + 'requires BYOC enrollment before %s makes a provider request', + async procedure => { + mockEnrollment.mockResolvedValue(false); + const caller = createCaller({ user: owner }); + + await expectSanitizedError(caller[procedure](setupInput), 'FORBIDDEN'); + + expect(mockEnrollment).toHaveBeenCalledWith(organizationId); + expect(fetchMock).not.toHaveBeenCalled(); + await expectNoSetup(); + } + ); + + it.each(providerProcedures)( + 'fails closed and sanitizes unavailable enrollment for %s', + async procedure => { + mockEnrollment.mockRejectedValue(new Error(`${TOKEN} ${PROVIDER_DETAIL}`)); + const caller = createCaller({ user: owner }); + + await expectSanitizedError(caller[procedure](setupInput), 'SERVICE_UNAVAILABLE'); + + expect(fetchMock).not.toHaveBeenCalled(); + await expectNoSetup(); + } + ); + + it.each(['', ' ', 'x'.repeat(4097)])( + 'rejects an empty or oversized token before provider requests', + async token => { + const caller = createCaller({ user: owner }); + for (const procedure of providerProcedures) { + await expect(caller[procedure]({ ...setupInput, token })).rejects.toMatchObject({ + code: 'BAD_REQUEST', + }); + } + expect(mockEnrollment).not.toHaveBeenCalled(); + expect(fetchMock).not.toHaveBeenCalled(); + await expectNoSetup(); + } + ); + + it('uses the same normalized token during discovery and setup', async () => { + const caller = createCaller({ user: owner }); + mockTeamDiscovery(); + await caller.discoverTeams({ organizationId, token: ` ${TOKEN}\n` }); + mockSelectionValidation(); + await caller.add({ ...setupInput, token: ` ${TOKEN}\n` }); + + for (const [, options] of fetchMock.mock.calls) { + expect(options?.headers).toMatchObject({ Authorization: `Bearer ${TOKEN}` }); + } + }); +}); + +describe('Personal Vercel compute authorization', () => { + it('allows an enrolled personal user to add a credential and starts the snapshot with userId', async () => { + const caller = createPersonalCaller({ user: personalUser }); + mockSelectionValidation(); + + const status = await caller.add({ token: TOKEN, teamId: TEAM.id, projectId: PROJECT.id }); + const credential = await getPersonalCredential(); + + expect(status).toMatchObject({ + userId: personalUser.id, + teamId: TEAM.id, + projectId: PROJECT.id, + setupStatus: 'pending', + setupStep: 'validating_access', + }); + expect(status).not.toHaveProperty('organizationId'); + expect(credential).toMatchObject({ + user_id: personalUser.id, + organization_id: null, + }); + expect(mockPersonalEnrollment).toHaveBeenCalledWith(personalUser.id); + expect(mockStartBuild).toHaveBeenCalledWith({ + userId: personalUser.id, + credentialId: status.credentialId, + buildGeneration: status.buildGeneration, + }); + }); + + it('hides and rejects setup for an unenrolled personal user without a credential', async () => { + mockPersonalEnrollment.mockResolvedValue(false); + const caller = createPersonalCaller({ user: personalUser }); + + await expect(caller.getEnrollment()).resolves.toEqual({ enrolled: false }); + await expectSanitizedError( + caller.add({ token: TOKEN, teamId: TEAM.id, projectId: PROJECT.id }), + 'FORBIDDEN' + ); + expect(fetchMock).not.toHaveBeenCalled(); + await expectNoPersonalSetup(); + }); + + it('keeps an unenrolled personal credential visible and allows removal', async () => { + const caller = createPersonalCaller({ user: personalUser }); + mockSelectionValidation(); + const added = await caller.add({ token: TOKEN, teamId: TEAM.id, projectId: PROJECT.id }); + mockStartBuild.mockClear(); + mockCleanupBuild.mockClear(); + mockPersonalEnrollment.mockResolvedValue(false); + + await expect(caller.getStatus()).resolves.toMatchObject({ userId: personalUser.id }); + await expectSanitizedError( + caller.add({ token: TOKEN, teamId: TEAM.id, projectId: PROJECT.id }), + 'FORBIDDEN' + ); + await expect(caller.remove({})).resolves.toEqual({ success: true }); + expect(mockCleanupBuild).toHaveBeenCalledWith({ + userId: personalUser.id, + credentialId: added.credentialId, + buildGeneration: added.buildGeneration, + }); + await expect(caller.getStatus()).resolves.toBeNull(); + }); + + it('does not expose a personal credential through the organization path', async () => { + const personalCaller = createPersonalCaller({ user: personalUser }); + mockSelectionValidation(); + await personalCaller.add({ token: TOKEN, teamId: TEAM.id, projectId: PROJECT.id }); + + await expect(createCaller({ user: owner }).getStatus({ organizationId })).resolves.toBeNull(); + }); +}); + +describe('Vercel discovery errors', () => { + it.each([ + [401, 'UNAUTHORIZED'], + [403, 'FORBIDDEN'], + [404, 'NOT_FOUND'], + [429, 'TOO_MANY_REQUESTS'], + [503, 'SERVICE_UNAVAILABLE'], + ] as const)('maps provider HTTP %s to sanitized application errors', async (status, code) => { + const caller = createCaller({ user: owner }); + fetchMock.mockImplementation(async () => + Response.json({ error: { message: `${TOKEN} ${PROVIDER_DETAIL}` } }, { status }) + ); + + for (const procedure of providerProcedures) { + await expectSanitizedError(caller[procedure](setupInput), code); + } + + await expect(caller.getStatus({ organizationId })).resolves.toBeNull(); + await expectNoSetup(); + }); + + it('rejects malformed provider responses without returning their contents or starting setup', async () => { + const caller = createCaller({ user: owner }); + fetchMock.mockImplementation(async () => + Response.json({ token: TOKEN, private: PROVIDER_DETAIL }) + ); + + for (const procedure of providerProcedures) { + await expectSanitizedError(caller[procedure](setupInput), 'BAD_GATEWAY'); + } + await expectNoSetup(); + }); + + it('does not expose network errors or their causes', async () => { + const caller = createCaller({ user: owner }); + fetchMock.mockRejectedValue(new Error(TOKEN, { cause: { secret: PROVIDER_DETAIL } })); + + await expectSanitizedError(caller.discoverTeams(setupInput), 'SERVICE_UNAVAILABLE'); + await expectNoSetup(); + }); +}); + +describe('Vercel setup validation', () => { + it('validates the selected team and project before storing an encrypted credential and starting setup', async () => { + const caller = createCaller({ user: owner }); + fetchMock.mockImplementation(async url => { + expect(await getCredential()).toBeUndefined(); + expect(mockStartBuild).not.toHaveBeenCalled(); + return Response.json(String(url).includes('/v2/teams/') ? TEAM : PROJECT); + }); + + const status = await caller.add(setupInput); + const credential = await getCredential(); + + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(status).toMatchObject({ + organizationId, + teamId: TEAM.id, + projectId: PROJECT.id, + setupStatus: 'pending', + setupStep: 'validating_access', + }); + expect(credential).toMatchObject({ + id: status.credentialId, + organization_id: organizationId, + team_id: TEAM.id, + project_id: PROJECT.id, + setup_status: 'pending', + token_encrypted: { + scheme: 'byoc-vercel-credential-rsa-aes-256-gcm', + keyId: 'agent-env-vars-v1', + }, + }); + expect(JSON.stringify(credential)).not.toContain(TOKEN); + expect(mockStartBuild).toHaveBeenCalledWith({ + organizationId, + credentialId: status.credentialId, + buildGeneration: status.buildGeneration, + }); + const savedStatus = await caller.getStatus({ organizationId }); + expect(savedStatus).toEqual(status); + expect(savedStatus).not.toHaveProperty('token'); + expect(savedStatus).not.toHaveProperty('token_encrypted'); + expect(JSON.stringify(savedStatus)).not.toContain(TOKEN); + }); + + it('revalidates a changed token after successful discovery without persisting it', async () => { + const caller = createCaller({ user: owner }); + mockTeamDiscovery(); + mockProjectDiscovery(); + await caller.discoverTeams(setupInput); + await caller.discoverProjects(setupInput); + fetchMock.mockResolvedValueOnce(Response.json({ error: TOKEN }, { status: 401 })); + + await expectSanitizedError( + caller.add({ ...setupInput, token: 'changed-invalid-token' }), + 'UNAUTHORIZED' + ); + + expect(fetchMock).toHaveBeenLastCalledWith( + `https://api.vercel.com/v2/teams/${TEAM.id}`, + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: 'Bearer changed-invalid-token' }), + }) + ); + await expectNoSetup(); + }); + + it('rejects a personally owned project before storing a credential or starting setup', async () => { + const caller = createCaller({ user: owner }); + const accountId = 'user_personal'; + fetchMock + .mockResolvedValueOnce(Response.json({ error: TOKEN }, { status: 403 })) + .mockResolvedValueOnce(Response.json({ ...PROJECT, accountId })); + + await expectSanitizedError(caller.add({ ...setupInput, teamId: accountId }), 'FORBIDDEN'); + await expectNoSetup(); + }); + + it.each([ + { teamId: 'team_other', projectId: PROJECT.id }, + { teamId: TEAM.id, projectId: 'prj_other' }, + ])('rejects a tampered selection $teamId/$projectId', async selection => { + const caller = createCaller({ user: owner }); + mockSelectionValidation(); + + await expectSanitizedError(caller.add({ ...setupInput, ...selection }), 'FORBIDDEN'); + await expectNoSetup(); + }); + + it('rejects a project that moved to another team after discovery', async () => { + const caller = createCaller({ user: owner }); + mockProjectDiscovery(); + await caller.discoverProjects(setupInput); + mockSelectionValidation({ ...PROJECT, accountId: 'team_other' }); + + await expectSanitizedError(caller.add(setupInput), 'FORBIDDEN'); + await expectNoSetup(); + }); + + it('rejects a project that was deleted after discovery', async () => { + const caller = createCaller({ user: owner }); + mockProjectDiscovery(); + await caller.discoverProjects(setupInput); + fetchMock + .mockResolvedValueOnce(Response.json(TEAM)) + .mockResolvedValueOnce(Response.json({ error: TOKEN }, { status: 404 })); + + await expectSanitizedError(caller.add(setupInput), 'NOT_FOUND'); + await expectNoSetup(); + }); + + it('keeps discovery read-only when credentials already exist and preserves the unique-organization guard', async () => { + const caller = createCaller({ user: owner }); + mockSelectionValidation(); + await caller.add(setupInput); + const existing = await getCredential(); + mockStartBuild.mockClear(); + mockTeamDiscovery(); + mockProjectDiscovery(); + + await caller.discoverTeams(setupInput); + await caller.discoverProjects(setupInput); + expect(await getCredential()).toEqual(existing); + expect(mockStartBuild).not.toHaveBeenCalled(); + + fetchMock.mockClear(); + await expectSanitizedError(caller.add(setupInput), 'CONFLICT'); + expect(fetchMock).not.toHaveBeenCalled(); + expect(await getCredential()).toEqual(existing); + }); + + it('reports a racing unique owner insert as a conflict', async () => { + const caller = createCaller({ user: owner }); + mockSelectionValidation(); + await caller.add(setupInput); + const current = await getCredential(); + mockStartBuild.mockClear(); + const lookup = jest + .spyOn(db.query.organization_vercel_compute_credentials, 'findFirst') + .mockResolvedValueOnce(undefined); + try { + mockSelectionValidation(); + await expectSanitizedError(caller.add(setupInput), 'CONFLICT'); + expect(await getCredential()).toEqual(current); + expect(mockStartBuild).not.toHaveBeenCalled(); + } finally { + lookup.mockRestore(); + } + }); + + it('reports other insert failures as retryable unavailability', async () => { + const caller = createCaller({ user: owner }); + mockSelectionValidation(); + const insert = jest.spyOn(db, 'insert').mockImplementationOnce(() => { + throw new Error(`${TOKEN} ${PROVIDER_DETAIL}`); + }); + try { + await expectSanitizedError(caller.add(setupInput), 'SERVICE_UNAVAILABLE'); + await expectNoSetup(); + } finally { + insert.mockRestore(); + } + }); + + it.each(['pending', 'building'] as const)( + 'rejects setup retry while setup is %s', + async status => { + const caller = createCaller({ user: owner }); + mockSelectionValidation(); + await caller.add(setupInput); + await db + .update(organization_vercel_compute_credentials) + .set({ setup_status: status }) + .where(eq(organization_vercel_compute_credentials.organization_id, organizationId)); + const before = await getCredential(); + mockStartBuild.mockClear(); + + await expectSanitizedError(caller.retrySetup({ organizationId }), 'PRECONDITION_FAILED'); + expect(await getCredential()).toEqual(before); + expect(mockCleanupBuild).not.toHaveBeenCalled(); + expect(mockStartBuild).not.toHaveBeenCalled(); + } + ); + + it('preserves failed setup, retry, and unenrolled removal without exposing the token', async () => { + const caller = createCaller({ user: owner }); + mockSelectionValidation(); + mockStartBuild.mockRejectedValueOnce(new Error(`${TOKEN} ${PROVIDER_DETAIL}`)); + + await expect(caller.add(setupInput)).rejects.toMatchObject({ + message: 'Cloud Agent snapshot build could not be started', + }); + const failed = await caller.getStatus({ organizationId }); + expect(failed).toMatchObject({ setupStatus: 'failed', setupError: 'setup_start_failed' }); + expect(JSON.stringify(failed)).not.toContain(TOKEN); + expect(JSON.stringify(failed)).not.toContain(PROVIDER_DETAIL); + + const retried = await caller.retrySetup({ organizationId }); + expect(retried.credentialId).toBe(failed?.credentialId); + expect(retried.buildGeneration).not.toBe(failed?.buildGeneration); + expect(retried.setupStatus).toBe('pending'); + expect(retried.setupError).toBeNull(); + expect(fetchMock).toHaveBeenCalledTimes(2); + + mockEnrollment.mockResolvedValue(false); + await expect(caller.remove({ organizationId })).resolves.toEqual({ success: true }); + await expect(caller.getStatus({ organizationId })).resolves.toBeNull(); + expect(mockCleanupBuild).toHaveBeenCalledTimes(2); + }); +}); + +describe('Vercel runtime upgrade', () => { + async function readyCredential() { + const caller = createCaller({ user: owner }); + mockSelectionValidation(); + await caller.add(setupInput); + const current = await getCredential(); + if (!current) throw new Error('Expected credential'); + const now = new Date().toISOString(); + await db + .update(organization_vercel_compute_credentials) + .set({ + setup_status: 'ready', + setup_step: null, + setup_error: null, + runtime_snapshot_id: 'snapshot-current', + project_slug: PROJECT.name, + runtime_digest: 'digest-old', + runtime_wrapper_version: '2.4.0', + runtime_released_at: '2026-09-01', + setup_completed_at: now, + }) + .where(eq(organization_vercel_compute_credentials.id, current.id)); + return current; + } + + it('rejects a stale setup retry without deleting a ready snapshot', async () => { + const caller = createCaller({ user: owner }); + await readyCredential(); + const before = await getCredential(); + mockStartBuild.mockClear(); + await expectSanitizedError(caller.retrySetup({ organizationId }), 'PRECONDITION_FAILED'); + expect(await getCredential()).toEqual(before); + expect(mockCleanupBuild).not.toHaveBeenCalled(); + expect(mockStartBuild).not.toHaveBeenCalled(); + }); + + it('starts an upgrade without removing the Ready snapshot', async () => { + const caller = createCaller({ user: owner }); + const current = await readyCredential(); + mockStartBuild.mockClear(); + mockCleanupBuild.mockClear(); + + const upgraded = await caller.upgradeRuntime({ organizationId }); + const row = await getCredential(); + + expect(upgraded.setupStatus).toBe('ready'); + expect(upgraded.runtimeSnapshotId).toBe('snapshot-current'); + expect(upgraded.upgradeStatus).toBe('pending'); + expect(upgraded.buildGeneration).not.toBe(current.build_generation); + expect(row).toMatchObject({ + setup_status: 'ready', + runtime_snapshot_id: 'snapshot-current', + upgrade_status: 'pending', + }); + expect(mockCleanupBuild).not.toHaveBeenCalled(); + expect(mockStartBuild).toHaveBeenCalledTimes(1); + }); + + it('marks upgrade failed without taking the org offline when start fails', async () => { + const caller = createCaller({ user: owner }); + await readyCredential(); + mockStartBuild.mockRejectedValueOnce(new Error('build start failed')); + + await expect(caller.upgradeRuntime({ organizationId })).rejects.toMatchObject({ + message: 'Cloud Agent snapshot build could not be started', + }); + expect(await getCredential()).toMatchObject({ + setup_status: 'ready', + runtime_snapshot_id: 'snapshot-current', + upgrade_status: 'failed', + upgrade_error: 'setup_start_failed', + }); + }); + + it('reports an upgrade when the Worker digest differs', async () => { + const caller = createCaller({ user: owner }); + await readyCredential(); + mockRuntimeIdentity.mockResolvedValueOnce({ + wrapperVersion: '2.4.0', + releasedAt: '2026-09-08', + digest: 'digest-new', + }); + + await expect(caller.getStatus({ organizationId })).resolves.toMatchObject({ + runtimeWrapperVersion: '2.4.0', + runtimeReleasedAt: '2026-09-01', + latestWrapperVersion: '2.4.0', + latestReleasedAt: '2026-09-08', + upgradeAvailable: true, + }); + }); +}); + +describe('Internal Vercel credential ownership', () => { + async function addPersonal() { + mockSelectionValidation(); + return createPersonalCaller({ user: personalUser }).add({ + token: TOKEN, + teamId: TEAM.id, + projectId: PROJECT.id, + }); + } + function request(credentialId: string, query: URLSearchParams, body?: unknown) { + return new NextRequest( + `http://localhost/api/internal/byoc/vercel-credentials/${credentialId}?${query}`, + { + method: body === undefined ? 'GET' : 'PATCH', + headers: { 'x-internal-api-key': INTERNAL_API_SECRET, 'content-type': 'application/json' }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + } + ); + } + it('reads an encoded personal owner and rejects another user or organization', async () => { + const added = await addPersonal(); + const params = Promise.resolve({ credentialId: added.credentialId }); + const own = await getInternalCredential( + request(added.credentialId, new URLSearchParams({ userId: personalUser.id })), + { params } + ); + expect(own.status).toBe(200); + expect(await own.json()).toMatchObject({ + userId: personalUser.id, + credentialId: added.credentialId, + }); + for (const query of [ + new URLSearchParams({ userId: outsider.id }), + new URLSearchParams({ organizationId }), + ]) { + const response = await getInternalCredential(request(added.credentialId, query), { params }); + expect(response.status).toBe(404); + } + }); + it('rejects two owner identities before reading the credential', async () => { + const added = await addPersonal(); + const response = await getInternalCredential( + request(added.credentialId, new URLSearchParams({ userId: personalUser.id, organizationId })), + { params: Promise.resolve({ credentialId: added.credentialId }) } + ); + expect(response.status).toBe(400); + }); + it('does not project personal build status through an organization identity', async () => { + const added = await addPersonal(); + const before = await getPersonalCredential(); + const projection = { + organizationId, + buildGeneration: added.buildGeneration, + setupStatus: 'failed', + setupStep: null, + setupError: 'setup_start_failed', + teamSlug: null, + projectSlug: null, + runtimeBuildId: null, + runtimeSnapshotId: null, + setupStartedAt: null, + setupCompletedAt: null, + }; + const params = Promise.resolve({ credentialId: added.credentialId }); + const wrong = await updateInternalCredential( + request(added.credentialId, new URLSearchParams(), projection), + { params } + ); + expect(wrong.status).toBe(200); + expect(await wrong.json()).toEqual({ updated: false }); + expect(await getPersonalCredential()).toEqual(before); + const { organizationId: _organizationId, ...fields } = projection; + const own = await updateInternalCredential( + request(added.credentialId, new URLSearchParams(), { ...fields, userId: personalUser.id }), + { params } + ); + expect(own.status).toBe(200); + expect(await getPersonalCredential()).toMatchObject({ + setup_status: 'failed', + setup_error: 'setup_start_failed', + }); + }); +}); diff --git a/apps/web/src/routers/organizations/organization-vercel-compute-router.ts b/apps/web/src/routers/organizations/organization-vercel-compute-router.ts new file mode 100644 index 00000000000..3f856e73faa --- /dev/null +++ b/apps/web/src/routers/organizations/organization-vercel-compute-router.ts @@ -0,0 +1,638 @@ +import 'server-only'; + +import { isUniqueViolation } from '@/lib/db-errors'; + +import * as z from 'zod'; +import { TRPCError } from '@trpc/server'; +import { and, eq } from 'drizzle-orm'; + +import { + organization_vercel_compute_credentials, + type OrganizationVercelComputeCredential, +} from '@kilocode/db/schema'; +import type { VercelComputeSetupStatus, VercelComputeSetupStep } from '@kilocode/db/schema-types'; +import { db } from '@kilocode/web-shared/lib/drizzle'; +import { encryptKeyedEnvelope, parseKeyedEnvelope } from '@kilocode/encryption'; +import { AGENT_ENV_VARS_PUBLIC_KEY } from '@kilocode/web-shared/lib/config.server'; +import { + cleanupVercelSnapshotBuild, + getVercelComputeEnrollment, + getVercelComputeEnrollmentForUser, + getVercelRuntimeIdentity, + startVercelSnapshotBuild, + type VercelSnapshotBuildStartInput, +} from '@/lib/cloud-agent-next/cloud-agent-client'; +import { createTRPCRouter, baseProcedure } from '@kilocode/web-shared/lib/trpc/init'; +import { + discoverVercelProjects, + discoverVercelTeams, + validateVercelSelection, + VercelApiError, +} from '@/lib/vercel-client'; +import type { Owner } from '@/lib/integrations/core/types'; +import { + organizationAdminProcedure, + OrganizationIdInputSchema, +} from '@kilocode/web-shared/routers/organizations/utils'; + +const VercelIdentifierSchema = z.string().trim().min(1).max(256); +const VercelTokenSchema = z.string().trim().min(1).max(4096); + +const OrganizationVercelComputeSetupInput = OrganizationIdInputSchema.extend({ + token: VercelTokenSchema, + teamId: VercelIdentifierSchema, + projectId: VercelIdentifierSchema, +}); +const PersonalVercelComputeSetupInput = z.object({ + token: VercelTokenSchema, + teamId: VercelIdentifierSchema, + projectId: VercelIdentifierSchema, +}); +const OrganizationVercelComputeDiscoveryInput = OrganizationIdInputSchema.extend({ + token: VercelTokenSchema, +}); +const PersonalVercelComputeDiscoveryInput = z.object({ token: VercelTokenSchema }); +const OrganizationVercelComputeProjectsInput = OrganizationIdInputSchema.extend({ + token: VercelTokenSchema, + teamId: VercelIdentifierSchema, +}); +const PersonalVercelComputeProjectsInput = z.object({ + token: VercelTokenSchema, + teamId: VercelIdentifierSchema, +}); +const VercelComputeRemoveInput = z.object({ + acknowledgeCleanupFailure: z.boolean().optional(), +}); + +const VercelComputeSetupStatusSchema = z.enum(['pending', 'building', 'ready', 'failed']); +const VercelComputeUpgradeStatusSchema = z.enum(['pending', 'building', 'failed']); + +type VercelComputeStatusCommon = { + credentialId: string; + teamId: string; + projectId: string; + teamSlug: string | null; + projectSlug: string | null; + setupStatus: z.infer; + setupStep: string | null; + setupError: string | null; + buildGeneration: string; + runtimeBuildId: string | null; + runtimeSnapshotId: string | null; + runtimeWrapperVersion: string | null; + runtimeReleasedAt: string | null; + runtimeDigest: string | null; + upgradeStatus: z.infer | null; + upgradeStep: string | null; + upgradeError: string | null; + latestWrapperVersion: string | null; + latestReleasedAt: string | null; + latestDigest: string | null; + upgradeAvailable: boolean; + setupStartedAt: string | null; + setupCompletedAt: string | null; +}; + +type VercelComputeStatus = + | (VercelComputeStatusCommon & { organizationId: string }) + | (VercelComputeStatusCommon & { userId: string }); + +function ownerCondition(owner: Owner) { + return owner.type === 'org' + ? eq(organization_vercel_compute_credentials.organization_id, owner.id) + : eq(organization_vercel_compute_credentials.user_id, owner.id); +} + +async function findVercelComputeCredential( + owner: Owner +): Promise { + return await db.query.organization_vercel_compute_credentials.findFirst({ + where: ownerCondition(owner), + }); +} + +function ownerSnapshotBuildInput( + owner: Owner, + credentialId: string, + buildGeneration: string +): VercelSnapshotBuildStartInput { + return { + ...(owner.type === 'org' ? { organizationId: owner.id } : { userId: owner.id }), + credentialId, + buildGeneration, + }; +} + +function ownerCredentialAad(owner: Owner, credentialId: string): string { + return owner.type === 'org' + ? `byoc-vercel-credential:v1:${owner.id}:${credentialId}` + : `byoc-vercel-credential:v1:user:${owner.id}:${credentialId}`; +} + +function statusFromRow( + row: OrganizationVercelComputeCredential, + owner: Owner +): VercelComputeStatus { + const common = { + credentialId: row.id, + teamId: row.team_id, + projectId: row.project_id, + teamSlug: row.team_slug, + projectSlug: row.project_slug, + setupStatus: row.setup_status, + setupStep: row.setup_step, + setupError: row.setup_error, + buildGeneration: row.build_generation, + runtimeBuildId: row.runtime_build_id, + runtimeSnapshotId: row.runtime_snapshot_id, + runtimeWrapperVersion: row.runtime_wrapper_version, + runtimeReleasedAt: row.runtime_released_at, + runtimeDigest: row.runtime_digest, + upgradeStatus: row.upgrade_status, + upgradeStep: row.upgrade_step, + upgradeError: row.upgrade_error, + latestWrapperVersion: null, + latestReleasedAt: null, + latestDigest: null, + upgradeAvailable: false, + setupStartedAt: row.setup_started_at ? new Date(row.setup_started_at).toISOString() : null, + setupCompletedAt: row.setup_completed_at + ? new Date(row.setup_completed_at).toISOString() + : null, + } satisfies VercelComputeStatusCommon; + + if (owner.type === 'org') { + if (row.organization_id === null) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Vercel compute credential is not organization-owned', + }); + } + return { ...common, organizationId: row.organization_id }; + } + + if (row.user_id === null) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Vercel compute credential is not user-owned', + }); + } + return { ...common, userId: row.user_id }; +} + +function runtimeBuildId(): string { + return `vercel-runtime-${crypto.randomUUID()}`; +} + +function newBuildGeneration(): string { + return crypto.randomUUID(); +} + +function safeSetupFailure(error: unknown): { setupError: string } { + if (error instanceof VercelSnapshotBuildStartError) { + return { setupError: 'cloud_agent_unavailable' }; + } + return { setupError: 'setup_start_failed' }; +} + +class VercelSnapshotBuildStartError extends Error { + constructor() { + super('Cloud Agent snapshot build could not be started'); + this.name = 'VercelSnapshotBuildStartError'; + } +} + +async function startBuildOrMarkFailed( + owner: Owner, + credentialId: string, + buildGeneration: string, + mode: 'setup' | 'upgrade' = 'setup' +): Promise { + try { + await startVercelSnapshotBuild(ownerSnapshotBuildInput(owner, credentialId, buildGeneration)); + } catch (error) { + const { setupError } = safeSetupFailure(error); + await db + .update(organization_vercel_compute_credentials) + .set( + mode === 'upgrade' + ? { + upgrade_status: 'failed', + upgrade_step: null, + upgrade_error: setupError, + } + : { + setup_status: 'failed', + setup_step: null, + setup_error: setupError, + } + ) + .where( + and( + ownerCondition(owner), + eq(organization_vercel_compute_credentials.id, credentialId), + eq(organization_vercel_compute_credentials.build_generation, buildGeneration) + ) + ); + throw new VercelSnapshotBuildStartError(); + } +} + +async function loadVercelComputeEnrollment(owner: Owner): Promise { + try { + return owner.type === 'org' + ? await getVercelComputeEnrollment(owner.id) + : await getVercelComputeEnrollmentForUser(owner.id); + } catch { + throw new TRPCError({ + code: 'SERVICE_UNAVAILABLE', + message: 'Vercel compute enrollment could not be verified', + }); + } +} + +async function requireVercelComputeEnrollment(owner: Owner): Promise { + if (await loadVercelComputeEnrollment(owner)) return; + + throw new TRPCError({ + code: 'FORBIDDEN', + message: + owner.type === 'org' + ? 'Customer-paid Vercel compute is not available for this organization' + : 'Customer-paid Vercel compute is not available for this user', + }); +} + +function rethrowVercelApiError(error: unknown): never { + const failure = + error instanceof VercelApiError ? error : new VercelApiError('SERVICE_UNAVAILABLE'); + throw new TRPCError({ code: failure.code, message: failure.message }); +} + +async function cleanupExistingVercelBuild( + owner: Owner, + credential: OrganizationVercelComputeCredential +): Promise { + if (owner.type === 'org' && credential.organization_id === null) return; + if (owner.type === 'user' && credential.user_id !== owner.id) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Vercel compute credential owner is inconsistent', + }); + } + + await cleanupVercelSnapshotBuild({ + ...ownerSnapshotBuildInput(owner, credential.id, credential.build_generation), + ...(credential.runtime_snapshot_id ? { snapshotId: credential.runtime_snapshot_id } : {}), + }); +} + +async function getStatusForOwner(owner: Owner): Promise { + const row = await findVercelComputeCredential(owner); + if (!row) return null; + + const status = statusFromRow(row, owner); + const latest = await getVercelRuntimeIdentity().catch(() => null); + if (!latest) return status; + return { + ...status, + latestWrapperVersion: latest.wrapperVersion, + latestReleasedAt: latest.releasedAt, + latestDigest: latest.digest, + upgradeAvailable: row.setup_status === 'ready' && row.runtime_digest !== latest.digest, + }; +} + +async function discoverTeamsForOwner(owner: Owner, token: string) { + await requireVercelComputeEnrollment(owner); + return await discoverVercelTeams(token).catch(rethrowVercelApiError); +} + +async function discoverProjectsForOwner(owner: Owner, token: string, teamId: string) { + await requireVercelComputeEnrollment(owner); + return await discoverVercelProjects(token, teamId).catch(rethrowVercelApiError); +} + +async function addForOwner( + owner: Owner, + input: { token: string; teamId: string; projectId: string } +): Promise { + await requireVercelComputeEnrollment(owner); + + const existing = await findVercelComputeCredential(owner); + if (existing) { + throw new TRPCError({ + code: 'CONFLICT', + message: 'Vercel compute is already configured. Remove it before adding new credentials.', + }); + } + + if (!AGENT_ENV_VARS_PUBLIC_KEY) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Vercel compute setup is not available', + }); + } + + const selection = await validateVercelSelection(input.token, input.teamId, input.projectId).catch( + rethrowVercelApiError + ); + + const credentialId = crypto.randomUUID(); + const buildGeneration = newBuildGeneration(); + const runtimeBuild = runtimeBuildId(); + const now = new Date().toISOString(); + const tokenEnvelopeScheme = 'byoc-vercel-credential-rsa-aes-256-gcm'; + const tokenEnvelopeKeyId = 'agent-env-vars-v1'; + const tokenEncrypted = parseKeyedEnvelope( + encryptKeyedEnvelope( + input.token, + tokenEnvelopeScheme, + { + keyId: tokenEnvelopeKeyId, + publicKeyPem: Buffer.from(AGENT_ENV_VARS_PUBLIC_KEY, 'base64'), + }, + ownerCredentialAad(owner, credentialId) + ), + tokenEnvelopeScheme + ); + + let row: OrganizationVercelComputeCredential; + try { + [row] = await db + .insert(organization_vercel_compute_credentials) + .values({ + id: credentialId, + organization_id: owner.type === 'org' ? owner.id : null, + user_id: owner.type === 'user' ? owner.id : null, + token_encrypted: tokenEncrypted, + token_scope: selection.tokenScope, + team_id: input.teamId, + project_id: input.projectId, + team_slug: selection.teamSlug, + project_slug: selection.projectSlug, + setup_status: 'pending', + setup_step: 'validating_access', + build_generation: buildGeneration, + runtime_build_id: runtimeBuild, + setup_started_at: now, + }) + .returning(); + } catch (error) { + if (isUniqueViolation(error)) { + throw new TRPCError({ + code: 'CONFLICT', + message: 'Vercel compute is already configured. Remove it before adding new credentials.', + }); + } + throw new TRPCError({ + code: 'SERVICE_UNAVAILABLE', + message: 'Vercel compute credentials could not be saved. Try again later.', + }); + } + + await startBuildOrMarkFailed(owner, row.id, buildGeneration); + + return statusFromRow(row, owner); +} + +async function retrySetupForOwner(owner: Owner): Promise { + await requireVercelComputeEnrollment(owner); + + const current = await findVercelComputeCredential(owner); + if (!current) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Vercel compute is not configured' }); + } + + if (current.setup_status !== 'failed') { + throw new TRPCError({ + code: 'PRECONDITION_FAILED', + message: 'Vercel compute setup must have failed before retrying', + }); + } + + try { + await cleanupExistingVercelBuild(owner, current); + } catch { + throw new TRPCError({ + code: 'SERVICE_UNAVAILABLE', + message: 'Existing Vercel compute resources could not be cleaned up. Try again later.', + }); + } + + const buildGeneration = newBuildGeneration(); + const runtimeBuild = runtimeBuildId(); + const now = new Date().toISOString(); + const [row] = await db + .update(organization_vercel_compute_credentials) + .set({ + setup_status: 'pending', + setup_step: 'validating_access', + setup_error: null, + runtime_build_id: runtimeBuild, + runtime_snapshot_id: null, + build_generation: buildGeneration, + setup_started_at: now, + setup_completed_at: null, + updated_at: now, + }) + .where( + and( + ownerCondition(owner), + eq(organization_vercel_compute_credentials.id, current.id), + eq(organization_vercel_compute_credentials.build_generation, current.build_generation), + eq(organization_vercel_compute_credentials.setup_status, 'failed') + ) + ) + .returning(); + + if (!row) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Vercel compute is not configured' }); + } + + await startBuildOrMarkFailed(owner, row.id, buildGeneration); + + return statusFromRow(row, owner); +} + +async function upgradeRuntimeForOwner(owner: Owner): Promise { + await requireVercelComputeEnrollment(owner); + + const current = await findVercelComputeCredential(owner); + if (!current) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Vercel compute is not configured' }); + } + if (current.setup_status !== 'ready' || !current.runtime_snapshot_id) { + throw new TRPCError({ + code: 'PRECONDITION_FAILED', + message: 'Vercel compute must be ready before upgrading the runtime', + }); + } + if (current.upgrade_status === 'pending' || current.upgrade_status === 'building') { + throw new TRPCError({ + code: 'CONFLICT', + message: 'A runtime upgrade is already in progress', + }); + } + + const buildGeneration = newBuildGeneration(); + const runtimeBuild = runtimeBuildId(); + const now = new Date().toISOString(); + const [row] = await db + .update(organization_vercel_compute_credentials) + .set({ + build_generation: buildGeneration, + runtime_build_id: runtimeBuild, + upgrade_status: 'pending', + upgrade_step: 'validating_access', + upgrade_error: null, + updated_at: now, + }) + .where( + and( + ownerCondition(owner), + eq(organization_vercel_compute_credentials.id, current.id), + eq(organization_vercel_compute_credentials.build_generation, current.build_generation) + ) + ) + .returning(); + + if (!row) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Vercel compute is not configured' }); + } + + await startBuildOrMarkFailed(owner, row.id, buildGeneration, 'upgrade'); + + return statusFromRow(row, owner); +} + +async function removeForOwner( + owner: Owner, + input: { acknowledgeCleanupFailure?: boolean } +): Promise<{ success: true }> { + const current = await findVercelComputeCredential(owner); + if (!current) return { success: true }; + + try { + await cleanupExistingVercelBuild(owner, current); + } catch { + if (!input.acknowledgeCleanupFailure) { + throw new TRPCError({ + code: 'PRECONDITION_FAILED', + message: + 'Vercel resources could not be cleaned up. Review existing snapshots and sandboxes before removing credentials.', + }); + } + } + + const [removed] = await db + .delete(organization_vercel_compute_credentials) + .where( + and( + ownerCondition(owner), + eq(organization_vercel_compute_credentials.id, current.id), + eq(organization_vercel_compute_credentials.build_generation, current.build_generation) + ) + ) + .returning({ id: organization_vercel_compute_credentials.id }); + + if (!removed) { + throw new TRPCError({ + code: 'CONFLICT', + message: 'Vercel compute configuration changed. Refresh and try again.', + }); + } + + return { success: true }; +} + +const organizationOwner = (organizationId: string): Owner => ({ type: 'org', id: organizationId }); +const userOwner = (userId: string): Owner => ({ type: 'user', id: userId }); + +export const organizationVercelComputeRouter = createTRPCRouter({ + getEnrollment: organizationAdminProcedure.query(async ({ input }) => ({ + enrolled: await loadVercelComputeEnrollment(organizationOwner(input.organizationId)), + })), + + getStatus: organizationAdminProcedure.query(async ({ input }) => { + return await getStatusForOwner(organizationOwner(input.organizationId)); + }), + + discoverTeams: organizationAdminProcedure + .input(OrganizationVercelComputeDiscoveryInput) + .mutation(async ({ input }) => { + return await discoverTeamsForOwner(organizationOwner(input.organizationId), input.token); + }), + + discoverProjects: organizationAdminProcedure + .input(OrganizationVercelComputeProjectsInput) + .mutation(async ({ input }) => { + return await discoverProjectsForOwner( + organizationOwner(input.organizationId), + input.token, + input.teamId + ); + }), + + add: organizationAdminProcedure + .input(OrganizationVercelComputeSetupInput) + .mutation(async ({ input }) => { + return await addForOwner(organizationOwner(input.organizationId), input); + }), + + retrySetup: organizationAdminProcedure.mutation(async ({ input }) => { + return await retrySetupForOwner(organizationOwner(input.organizationId)); + }), + + upgradeRuntime: organizationAdminProcedure.mutation(async ({ input }) => { + return await upgradeRuntimeForOwner(organizationOwner(input.organizationId)); + }), + + remove: organizationAdminProcedure + .input(OrganizationIdInputSchema.extend(VercelComputeRemoveInput.shape)) + .mutation(async ({ input }) => { + return await removeForOwner(organizationOwner(input.organizationId), input); + }), +}); + +export const personalVercelComputeRouter = createTRPCRouter({ + getEnrollment: baseProcedure.query(async ({ ctx }) => ({ + enrolled: await loadVercelComputeEnrollment(userOwner(ctx.user.id)), + })), + + getStatus: baseProcedure.query(async ({ ctx }) => { + return await getStatusForOwner(userOwner(ctx.user.id)); + }), + + discoverTeams: baseProcedure + .input(PersonalVercelComputeDiscoveryInput) + .mutation(async ({ ctx, input }) => { + return await discoverTeamsForOwner(userOwner(ctx.user.id), input.token); + }), + + discoverProjects: baseProcedure + .input(PersonalVercelComputeProjectsInput) + .mutation(async ({ ctx, input }) => { + return await discoverProjectsForOwner(userOwner(ctx.user.id), input.token, input.teamId); + }), + + add: baseProcedure.input(PersonalVercelComputeSetupInput).mutation(async ({ ctx, input }) => { + return await addForOwner(userOwner(ctx.user.id), input); + }), + + retrySetup: baseProcedure.mutation(async ({ ctx }) => { + return await retrySetupForOwner(userOwner(ctx.user.id)); + }), + + upgradeRuntime: baseProcedure.mutation(async ({ ctx }) => { + return await upgradeRuntimeForOwner(userOwner(ctx.user.id)); + }), + + remove: baseProcedure + .input(VercelComputeRemoveInput.optional()) + .mutation(async ({ ctx, input }) => { + return await removeForOwner(userOwner(ctx.user.id), input ?? {}); + }), +}); + +export type { VercelComputeSetupStatus, VercelComputeSetupStep }; diff --git a/apps/web/src/routers/root-router.ts b/apps/web/src/routers/root-router.ts index 1c2cc2fd39a..0768845d0ac 100644 --- a/apps/web/src/routers/root-router.ts +++ b/apps/web/src/routers/root-router.ts @@ -49,7 +49,9 @@ import { moderationRouter } from '@/routers/moderation-router'; import { userExportsRouter } from '@/routers/user-exports-router'; import { spendAlertRouter } from '@/routers/spend-alert-router'; import { agentBriefingRouter } from '@/routers/agent-briefing-router'; +import { personalVercelComputeRouter } from '@/routers/organizations/organization-vercel-compute-router'; export const rootRouter = createTRPCRouter({ + vercelCompute: personalVercelComputeRouter, test: testRouter, organizations: organizationsRouter, debug: debugRouter, diff --git a/apps/web/src/routers/webhook-triggers-router.ts b/apps/web/src/routers/webhook-triggers-router.ts index db9d1799f86..079b33368e7 100644 --- a/apps/web/src/routers/webhook-triggers-router.ts +++ b/apps/web/src/routers/webhook-triggers-router.ts @@ -20,7 +20,7 @@ import { type User, } from '@kilocode/db/schema'; import { - getKiloSandboxAllocation, + getSandboxAllocation, isSandboxAllocationAvailable, sandboxAllocationSchema, type SandboxAllocation, @@ -438,7 +438,7 @@ async function assertSandboxAllocationAvailable( if (isSandboxAllocationAvailable(capabilities, sandboxAllocation)) return; const option = capabilities.options.find( - candidate => getKiloSandboxAllocation(candidate.allocation) === sandboxAllocation + candidate => getSandboxAllocation(candidate.allocation) === sandboxAllocation ); const unavailableReason = option?.unavailableReason ?? option?.repositoryUnavailableReason; diff --git a/apps/web/src/scripts/mcp-catalog/catalog.test.ts b/apps/web/src/scripts/mcp-catalog/catalog.test.ts index 3eef0d0b3c5..d29ba75acad 100644 --- a/apps/web/src/scripts/mcp-catalog/catalog.test.ts +++ b/apps/web/src/scripts/mcp-catalog/catalog.test.ts @@ -141,6 +141,23 @@ describe('mcp-catalog catalog', () => { expect(isDenylistedPath('activeSessions.getToken')).toBe(false); }); + it('withholds Vercel secrets and resource mutations while retaining status reads', () => { + for (const prefix of ['vercelCompute', 'organizations.vercelCompute']) { + for (const method of [ + 'add', + 'discoverTeams', + 'discoverProjects', + 'remove', + 'retrySetup', + 'upgradeRuntime', + ]) { + expect(isDenylistedPath(`${prefix}.${method}`)).toBe(true); + } + expect(isDenylistedPath(`${prefix}.getStatus`)).toBe(false); + expect(isDenylistedPath(`${prefix}.getEnrollment`)).toBe(false); + } + }); + it('withholds briefing cost confirmation while retaining preparation and read access', () => { const leaves = collectCatalogLeaves(rootRouter).filter(leaf => leaf.path.startsWith('agentBriefing.') diff --git a/apps/web/src/scripts/mcp-catalog/catalog.ts b/apps/web/src/scripts/mcp-catalog/catalog.ts index 4f0ab89cb7d..e8df67dff3f 100644 --- a/apps/web/src/scripts/mcp-catalog/catalog.ts +++ b/apps/web/src/scripts/mcp-catalog/catalog.ts @@ -46,6 +46,19 @@ export const ROOT_ROUTER_PATH = join(__dirname, '..', '..', 'routers', 'root-rou * agent could otherwise mint the estimate token itself and bypass that consent. */ const NATIVE_ONLY_PATHS: Record = { + // Vercel connection uses raw credentials; teardown and builds require app confirmation. + 'vercelCompute.add': true, + 'vercelCompute.discoverProjects': true, + 'vercelCompute.discoverTeams': true, + 'vercelCompute.remove': true, + 'vercelCompute.retrySetup': true, + 'vercelCompute.upgradeRuntime': true, + 'organizations.vercelCompute.add': true, + 'organizations.vercelCompute.discoverProjects': true, + 'organizations.vercelCompute.discoverTeams': true, + 'organizations.vercelCompute.remove': true, + 'organizations.vercelCompute.retrySetup': true, + 'organizations.vercelCompute.upgradeRuntime': true, 'activeSessions.widgetCredential': true, 'agentBriefing.createOneOff': true, 'agentBriefing.generateSummary': true, diff --git a/docs/2026-09-09-integrations-cloud-personal-byoc-design.md b/docs/2026-09-09-integrations-cloud-personal-byoc-design.md new file mode 100644 index 00000000000..6834261c0d7 --- /dev/null +++ b/docs/2026-09-09-integrations-cloud-personal-byoc-design.md @@ -0,0 +1,147 @@ +# Integrations Cloud hub and personal BYOC + +**Status: UI placement superseded; personal Vercel ownership retained, 2026-10-13.** + +Vercel compute setup now belongs in **Cloud Agent settings → Compute**. +The active implementation supports personal and organization-owned Vercel credentials. +The former organization Compute URL redirects to that settings tab. +The personal credential changes now use the current control plane and settings dialog. +The Integrations Cloud hub remains outside this stack. +Kubernetes, E2B, and other BYOC providers remain outside this scope. + +The text below records the earlier design. Its UI placement does not define current behavior. +See the current scope in [the Vercel implementation plan](byoc-vercel-implementation-plan.md). + +Approved 2026-09-09. Moves Vercel compute setup into Integrations and extends customer-paid Vercel (BYOC) to personal Kilo accounts. + +## Goal + +- Put compute in Integrations behind one **Cloud** card that opens a provider list. +- Ship **Vercel** as the only provider. +- Let an enrolled personal user connect Vercel for that user’s personal Cloud Agent sessions only. +- Keep org BYOC isolated from personal BYOC. + +## Non-goals + +- Other providers (AWS, E2B, and similar). +- Hobby-owned Vercel projects (`accountId` that does not start with `team_`). +- Sharing a personal Vercel credential with an organization. +- Redirects from `/organizations/:id/cloud/compute`. +- Browser/E2E coverage (manual live Vercel setup remains a manual check). +- Keeping `BYOC_VERCEL_ORG_IDS` as a second allowlist. + +## Surfaces + +Personal Integrations (`/integrations`) and org Integrations (`/organizations/:id/integrations`) show a **Cloud** hub card in the existing grid. Cloud is a hub, not an OAuth platform in `PLATFORM_DEFINITIONS`. + +Visibility: + +- Hidden unless the current owner is enrolled in `BYOC_VERCEL_IDS`, or that owner already has a credential row (so disconnect remains possible after enrollment is removed). +- Connect/setup is allowed only when enrolled. +- Personal: the signed-in user is the owner. +- Org: same roles as today’s Compute nav — organization **owner** and **admin** only. Members and billing managers do not see the card. + +Routes: + +- Hub: `/integrations/cloud` and `/organizations/:id/integrations/cloud` +- Vercel: `/integrations/cloud/vercel` and `/organizations/:id/integrations/cloud/vercel` + +The hub lists providers. Vercel is the only card. Do not show coming-soon placeholders. + +The Vercel page is Integrations-style: connected / not connected, connect, disconnect. The steps stay: masked token → Continue → team/project → Start setup. + +Remove org **Cloud → Compute** from the sidebar and delete the Compute page. Chat, sessions, webhooks, triggers, and MCP stay under Cloud. No redirect; the old URL is gone. + +## Enrollment + +Replace `BYOC_VERCEL_ORG_IDS` with `BYOC_VERCEL_IDS`. + +- Comma-separated user IDs and organization IDs. +- Empty is off. +- `*` admits every personal user and every organization. +- Do not read the old name. Callers rename the variable. + +Worker enrollment stays authoritative. Web UI and tRPC also hide/reject setup when the owner is not enrolled, so the card is not a second policy. + +## Ownership + +A credential has exactly one owner: a user **or** an organization. + +- Personal Cloud Agent sessions (`organization_id` absent) use that user’s credential, and only if the user is enrolled. +- Org sessions use that org’s credential, and only if the org is enrolled. +- An enrolled owner without a ready credential fails closed. No platform-paid fallback. +- Code Reviewer sessions (`billingOrigin === 'code-review'`) stay on platform compute. Do not broaden that exception unless a new origin is named. + +Siblings keep the same immutable binding (owner + credential id). Missing or swapped binding rejects. + +## Data model + +Keep table `organization_vercel_compute_credentials`. Do not rename it in this change. + +- Add nullable `user_id` as `text` (Kilo user IDs are not always UUIDs; OAuth ids often start with `oauth/`). +- Make `organization_id` nullable. +- Check: exactly one of `organization_id` or `user_id` is set. +- Unique on `organization_id` where present; unique on `user_id` where present. One credential per owner. +- Existing org rows stay org-owned. No backfill. + +Token envelope stays BYOC RSA (`byoc-vercel-credential-rsa-aes-256-gcm`). + +- Org decrypt context stays `byoc-vercel-credential:v1:${organizationId}:${credentialId}`. Do not re-encrypt existing org rows. +- Personal encrypt/decrypt context is `byoc-vercel-credential:v1:user:${userId}:${credentialId}`. +- Decrypt must fail if the owner in context does not match the row. + +`softDeleteUser` must delete a personal credential. Org credentials stay with the organization. + +## APIs + +Generalize the existing org Vercel compute router to an owner (`user` | `org`), matching Integrations `Owner`. Personal procedures authorize the signed-in user. Org procedures stay owner/admin. + +Two enrollment surfaces, both need a user-owner variant. Path-encode owner ids (`encodeURIComponent`) so `oauth/…` ids work. + +1. Worker → web credential lookup (existing org URLs): + - Credential GET already takes `credentialId` plus owner. Accept exactly one of `organizationId` or `userId`. + - Enrollment GET: keep `/api/internal/byoc/vercel-credentials/organization/:organizationId`. Add `/api/internal/byoc/vercel-credentials/user/:userId`. +2. Web → Worker status (`getVercelComputeEnrollment`): + - Keep `GET /internal/byoc/vercel-enrollment/:organizationId`. + - Add `GET /internal/byoc/vercel-enrollment/user/:userId` (do not put a user id in the organization path). + +Credential JSON, Worker Zod, snapshot-build DO, `SandboxProviderBinding`, and credential PATCH use a discriminated owner: org rows keep `organizationId`; personal rows send `userId`. Never put `userId` in `organizationId`. Personal `userId` is text; do not validate user ids as UUIDs. Response owner must match the request. + +## Runtime + +At session start the Worker reads `BYOC_VERCEL_IDS` and the matching credential. + +- Org session: org id in the list and a ready org credential. Binding is org + credential id. +- Personal session: session user id in the list and a ready user credential. Binding is user + credential id. +- Team-scoped tokens still send Vercel `teamId`. Project-scoped tokens still omit it. +- Setup still uses the snapshot-build path. Public `setupStep` stays null until ready/failed, same as today. + +Discovery and validation stay fail-closed: Vercel teams only (`team_…`), exact project/account match, 20 pages / 1,000 results / 10s deadline. Hobby-owned projects stay `FORBIDDEN`. + +Never log tokens, envelopes, or auth headers. + +## Errors and recovery + +Disconnect removes the credential and its setup resources, same as Compute today. + +If the owner is not enrolled and has no credential, the Cloud card is absent and setup APIs reject. If a credential row exists, the owner can open Cloud and disconnect even when no longer enrolled. Connect stays rejected until they are enrolled again. + +Failed setup stays visible and retryable on the Vercel page. It does not take down other Integrations. + +## Testing + +Cover the new contracts. Do not retest the Vercel client unless this change edits it. + +- Schema: exactly one owner; unique per user and per org; `user_id` accepts non-UUID ids. +- Web router: enrolled personal user can set up; unenrolled is hidden/rejected; org stays owner/admin; a personal credential cannot be read as an org credential. +- Envelope: decrypt fails if owner id does not match. +- Worker: personal session with ready user credential enrolls; enrolled personal user without credential fails closed; org path unchanged; Code Reviewer stays platform-paid. +- User deletion removes the personal credential. + +No browser/E2E in this change. + +## Out of scope follow-ups + +- Additional Cloud providers on the hub. +- Hobby Vercel accounts. +- Renaming the credential table. diff --git a/docs/byoc-architecture-research.md b/docs/byoc-architecture-research.md new file mode 100644 index 00000000000..a24bfda4282 --- /dev/null +++ b/docs/byoc-architecture-research.md @@ -0,0 +1,332 @@ +# BYOC (Bring Your Own Cloud) Architecture for Cloud Agent Next + +**Author:** Kilo Agent +**Date:** August 2026 +**Status:** Proposal / Research — rewritten against the `sticky-slider` call-home control plane +**Target Service:** `services/cloud-agent-next` +**Canonical control-plane spec:** `sticky-slider` worktree, `CALL-HOME-CONTROL-PLANE.md` + +--- + +## 1. Executive Summary + +BYOC for `cloud-agent-next` is not a family of per-vendor sandbox adapters talking inbound HTTP to a wrapper. That model is being replaced. + +The control plane under construction in `sticky-slider` is: + +- one owner-scoped logical sandbox; +- one `SandboxControl` Durable Object; +- one wrapper process and one shared Kilo backend; +- one outbound WebSocket from the wrapper to `SandboxControl`; +- many concurrent Cloud Agent sessions on that sandbox; +- provider adapters that only create, observe, stop, extend a lease, and fetch logs. + +That remains the density target. The first customer-paid Vercel MVP is a +deliberate exception: it uses one isolated `ses-*` sandbox per session. This +avoids repinning an existing shared `SandboxControl` across Cloudflare, +Kilo-paid Vercel, and customer-paid Vercel. The implementation plan is the +authoritative source for Stage 1 behavior. + +BYOC is then a choice of **who pays for and where the physical instance runs**. E2B, Modal, Vercel, a customer VPC runner, and Cloudflare Containers all speak the same call-home protocol. They differ only in the five provider operations and whether a stopped instance is resumable. + +**Do not** implement `E2BAgentSandbox` against today's `AgentSandbox` / file-hop / inbound-wrapper protocol. **Do not** treat an enterprise outbound runner as a later, separate architecture. Both land on `SandboxControl` after that plane exists. + +**Do not** take ComputeSDK as a runtime dependency. That verdict is unchanged. + +--- + +## 2. What changed since the first draft + +The first draft of this document (early August 2026) treated `sticky-slider`'s Vercel work as a realized multi-backend prototype: thin REST clients, pre-baked snapshots, and `VercelWrapperTransport` (file write → `curl` → file read) because Workers cannot open TCP into a Vercel microVM. + +That prototype proved two useful things and one thing we should not copy: + +| Keep | Drop | +|---|---| +| Worker-native `fetch` REST clients, no Node SDKs | File-hop as a control transport | +| Pre-baked snapshots / templates with Bun + wrapper + CLI | Per-session `AgentSandbox` as the BYOC seam | +| Two-phase create intents and stop tombstones | Inbound wrapper HTTP / PTY as the control path | +| Encrypted customer credentials | `wrapperRunId` / generation fences as session identity | + +`sticky-slider` then replaced the control path. As of 2026-08-25: + +- Architecture decisions in `CALL-HOME-CONTROL-PLANE.md` are implemented on the + latest local `sticky-slider`; production enrollment remains off. +- A real Vercel snapshot VM completed authenticated WSS hello, status, drop, + and reconnect against `SandboxControl`. Outbound WSS is proven. +- Protocol schemas, opaque sandbox credentials, hibernating sockets, + hello-replace, physical lifecycle, session integration, idle stop, and the + five-op provider adapter exist. +- There is no transport migration or file-hop compatibility path. Dual transport is forbidden. + +The rest of this document is written against that target, not against today's production session DO. + +--- + +## 3. Target architecture + +This is the eventual shared-sandbox target, not the customer Vercel Stage 1 +topology. Stage 1 creates one `SandboxControl` and sandbox per session. + +```text + Cloudflare + + browser A ── WS ──► Session A DO ─┐ + browser B ── WS ──► Session B DO ─┼─ typed DO RPC ─► SandboxControl DO + browser C ── WS ──► Session C DO ─┘ ║ + ║ one outbound WS + ║ + wrapper control plane + │ + one shared Kilo backend + │ + directory A / B / C + Kilo sessions +``` + +### 3.1 Ownership + +| Owner | Owns | Does not own | +|---|---|---| +| **Session DO** (`sessionId`) | Auth, chat, messages, idempotency, client streams, per-message no-progress deadline | Wrapper socket, provider API, physical health, idle stop | +| **`SandboxControl` DO** (`sandboxId`) | Physical start/observe/stop, credential, the one wrapper socket, heartbeats, capacity, idle shutdown, session routing, every sandbox-lifetime deadline | Prompts, chat history, message terminalization | +| **Wrapper** | One Kilo backend, global event feed, explicit directory + `kiloSessionId` on every call, reconnect | User authorization, Cloud message queues | +| **Provider adapter** | `create`, `observe`, `stop`, `ensureLeaseAtLeast`, `logs`, plus a `resumable` bit | States, deadlines, intents, tombstones, timeout semantics | + +Rule: Session DOs decide what should happen. `SandboxControl` decides how to reach and operate the sandbox. + +### 3.2 Provider boundary + +Providers have incompatible native timeout models. One sleeps a container; another gives a total duration and destroys the machine. Neither model may appear in the state machine. + +A provider adapter supplies exactly five operations: + +| Operation | Contract | +|---|---| +| `create(intent)` | Start an instance for a durable intent that already exists. May return an instance id, or report unresolved. | +| `observe(ref)` | Report `active`, `terminal`, or **`unknown`**. A boolean is not sufficient. | +| `stop(ref)` | Attempt a stop; report terminal or retryable. | +| `ensureLeaseAtLeast(ms)` | Guarantee at least `ms` of remaining provider lifetime. How (activity ping vs duration extend) is private to the adapter. | +| `logs(ref)` | Best-effort diagnostics for a failing instance. | + +One capability bit: whether a stopped instance is **resumable**. Durable records carry an opaque `providerRef` that only the adapter parses. + +The create intent is persisted **before** the create call. The instance reference is cleared only **after** the provider confirms terminal. Those two asymmetries are the anti-leak guarantee, and they live in `SandboxControl`, not in each adapter. + +### 3.3 Network + +The wrapper WebSocket is the only interactive network path between the sandbox and the Cloud control plane: + +```text +GET /sandbox-control/{sandboxId} +Upgrade: websocket +Authorization: Bearer +``` + +Bulk artifacts (snapshots, attachments, logs) may use scoped HTTP object-storage URLs. They are data transfer, not a second control transport. + +This is the property BYOC actually needs: + +- Cloudflare Workers never open inbound TCP/HTTP into the customer's compute. +- Customer VPCs never open inbound ports. The wrapper dials out on 443. +- Interactive PTY, if added later, is typed binary frames on the same socket (or a second socket only if measurement demands it). It is not a public wrapper port. + +### 3.4 Identity + +Durable: `sandboxId` (logical, survives physical replacement), `sessionId`, `kiloSessionId`, `messageId`, `providerInstanceId` (internal to `SandboxControl`). + +Removed: `wrapperId`, `wrapperRunId`, `wrapperGeneration`, `wrapperConnectionId`, transport-mode flags. + +The sandbox credential is an opaque 256-bit capability for one logical sandbox's wrapper connection. It is not a Kilo JWT or a per-session dispatch ticket. Physical replacement rotates it. + +### 3.5 Sharing model + +One sandbox never mixes owners. All sessions in a sandbox use the same Kilo credentials. Session-varying repository, tool, and workspace inputs are passed explicitly or stored under that session's directory, never installed by mutating shared `process.env`. + +This is the VS Code extension model (one `kilo serve`, many worktrees) with Cloud-side owner and authorization boundaries. Resource contention in one Kilo process is a measured risk, not a reason to start with per-session processes. + +--- + +## 4. What the first draft got right + +These still hold. + +**Control plane vs compute plane.** Cloudflare Worker + Durable Objects own auth, transcripts, event ordering, billing, and UI streaming. Customer compute owns checkout, tools, compilers, tests, and the local Kilo runtime. + +**Outbound ingest / call-home is the enterprise requirement.** Zero inbound firewall openings in the customer VPC. + +**Do not use ComputeSDK as a runtime dependency.** It models sandboxes as ephemeral command runners, imports Node built-ins that fail in Workers, has no PTY or durable lifecycle, and would fight `kilocode-wrapper.js` with a second daemon. Use it only as a catalog of vendor REST endpoints and auth header names. + +**Encrypted customer credentials.** Provider keys are stored encrypted and +decrypted only by the control-plane worker that needs them. Customer Vercel +Stage 1 reuses the existing `AGENT_ENV_VARS` RSA pair and does not support key +rotation. Credentials are never logged. + +**Thin native REST clients.** When a provider needs a control API (create/stop/extend), implement it with Web `fetch` in the Worker, the way `sticky-slider` talks to Vercel. Do not pull vendor Node SDKs into the isolate. + +**Standardized image / snapshot.** Publish `ghcr.io/kilocode/agent-sandbox` and pre-baked templates so every provider boots the same wrapper. The wrapper always calls home; the image does not grow a per-provider control server. + +--- + +## 5. What the first draft got wrong + +| First draft | Target | +|---|---| +| Phase 1 = `E2BAgentSandbox` implementing `AgentSandbox` (`ensureWrapper`, HTTP RPC, PTY) | Phase 1 = finish `SandboxControl`. A provider is the five operations in §3.2. | +| Copy `VercelWrapperTransport` file-hop for providers without inbound TCP | There is no file-hop, dual transport, or inbound wrapper HTTP. | +| One Cloud session → one VM as the universal target | One owner sandbox → many chats is the eventual density model. Customer Vercel Stage 1 intentionally starts per-session. | +| Enterprise runner (`kilocode-runner`) is Phase 2 architecture | Same protocol as E2B/Vercel. The runner is a provider that the customer operates. | +| `CloudAgentSession` DO remains the coordinator of compute | Session DO owns the chat. `SandboxControl` owns compute. | +| Interactive PTY is a native provider feature required for MVP | Terminal is explicitly out of v1. Add later as typed frames on the call-home socket. | +| Implementation effort estimated in "adapter weeks" against today's protocol | Calendar is gated on sticky-slider slices 5–8, then a thin adapter. | + +The existing `AgentSandbox` / `AgentSandboxLifecycle` types remain the production Cloudflare/Vercel seam until call-home ships. They are not the BYOC extension point. + +--- + +## 6. BYOC deployment models (revised) + +The four customer-facing models are unchanged. The implementation of each is now "adapter + call-home", not "new control plane". + +### 6.1 Keyed cloud microVMs (E2B / Modal / Vercel) + +User pastes an API key. `SandboxControl` calls the vendor REST API, injects the opaque sandbox credential and call-home URL, boots from the pre-baked template. Wrapper dials `/sandbox-control/{sandboxId}`. + +- Lowest setup friction. +- Fast cold start (sub-second to a few seconds). +- Compute is still on the vendor's SaaS, not in the customer's VPC. +- Vercel is already the first physical host of this model in `sticky-slider`, and the easiest **customer-keyed** MVP: org-stored token + team + project, BYOC setup copies/builds the snapshot into their project, then the same adapter creates sandboxes they pay for. E2B is a later second adapter, not the first BYOC slice. + +### 6.2 Major-cloud serverless containers (ECS / ACA / Cloud Run) + +Customer provides IAM. Worker starts a task from `ghcr.io/kilocode/agent-sandbox`. Wrapper still dials out. + +- Satisfies "run in our AWS/GCP/Azure account". +- Cold starts are worse (especially ECS Fargate). +- The first draft's "inbound HTTP RPC needs a public IP or reverse tunnel" problem **disappears**. Call-home removed that requirement. + +### 6.3 Self-hosted Docker host + +A small daemon next to `docker.sock` accepts create/stop from… nowhere inbound. Either: + +- the daemon is itself a `kilocode-runner` that already has an outbound socket, and it starts sibling containers; or +- we do not support raw Docker daemon exposure. + +Do not expose Docker over the internet. The Roomote local-Docker pattern is only acceptable behind the outbound runner. + +### 6.4 Enterprise outbound runner (`kilocode-runner`) + +Customer installs a Helm chart / binary. The runner maintains an outbound connection to Kilocode Cloud, receives create intents, starts pods, injects the sandbox credential, and lets each wrapper open its own call-home WebSocket (or brokers that socket). + +- 100% data/compute sovereignty, zero inbound ports, private LLMs. +- This is not a second protocol. It is a provider whose `create`/`observe`/`stop` happen inside the customer's cluster instead of via a public SaaS API. +- Higher customer DevOps cost; implement after keyed microVMs, not instead of `SandboxControl`. + +--- + +## 7. Comparison + +| Criteria | Keyed microVMs | Cloud serverless | Docker host via runner | Enterprise runner | +|---|---|---|---|---| +| Audience | Pro users, startups | Mid-market cloud orgs | Homelabs | Regulated enterprises | +| Setup | Paste API key | IAM / VPC | Helm or daemon + Docker | Helm | +| Cold start | Sub-second–2s | 1s–30s | 1s–3s | 2s–5s warm | +| Inbound firewall | None | **None** (call-home) | None (via runner) | None | +| Data sovereignty | Vendor SaaS | Customer cloud account | Local machine | Customer VPC / on-prem | +| PTY in v1 | Out of scope for all | Out of scope | Out of scope | Out of scope | +| Adapter size once call-home exists | Small REST client | Small REST / IAM client | Runner process | Runner process | +| Depends on | `SandboxControl` shipping | Same | Same + runner | Same + runner | + +--- + +## 8. Recommended plan + +### Stage 0 — Call-home control plane (`sticky-slider`, implemented; enrollment off) + +This remains the required base. Do not start a BYOC provider against the old seam. + +The landed base includes: + +1. `SandboxControl` physical lifecycle, `providerRef`, attached-session routes, + one-alarm deadline table, status projection, and transition log. +2. Shared wrapper call-home client and one `kilo serve` across directories. +3. Session DO integration, idle stop/replacement/lazy restore, and the normalized + five-operation provider adapter. + +Kilo-side `submitOrGet` / live `importOrGetSession` live in kilo-cli and are out of this repo. Do not fake them in the wrapper. + +Production enrollment stays off while the lifecycle billing-safety fixes and +customer Vercel provider spike in the implementation plan are completed. + +### Stage 1 — First keyed BYOC provider (customer Vercel) + +Only after Stage 0's provider adapter exists. Do not start E2B first. + +Customer Vercel is BYOC: they bring a token for a team they already pay. The REST client, snapshot pipeline, and outbound WSS path already exist. The MVP is credential routing plus one setup step, not a new provider. + +1. Org setting: encrypted `VERCEL_TOKEN` + `VERCEL_TEAM_ID` + `VERCEL_PROJECT_ID` (not worker env). +2. **BYOC setup** (required, once per org / when the runtime image changes): using those credentials, copy or build the Kilo wrapper snapshot into **their** project. Persist the resulting `snapshotId` + `runtimeBuildId` with the org record. Reuse `scripts/vercel-snapshot.ts`; do not assume Kilo's `snap_*` is valid on their token. +3. Same five-op Vercel adapter. Creates boot from the org's snapshot. `SandboxControl` does not grow a Vercel state machine. +4. Wrapper only calls home. No public wrapper port. +5. Prove: setup writes a snapshot in the customer project; two sessions create + two distinct customer-paid `ses-*` sandboxes; idle stop and reconciliation + work; removing the credential makes subsequent control fail closed. + +E2B/Modal are the same adapter shape later if we want a one-key UX or a non-Vercel vendor. They are not the MVP. + +### Stage 2 — Enterprise runner + +Same five operations, executed by a customer-deployed runner instead of a SaaS API. + +1. Runner registers, authenticates, receives create intents over its outbound connection. +2. Starts a pod/container from the standard image, injects sandbox credential + call-home URL. +3. Wrapper opens `/sandbox-control/{sandboxId}` as in Stage 1. +4. Optional: runner brokers the socket if the pod cannot reach Kilocode directly (air-gap with an HTTP proxy is enough; a second custom protocol is not). + +### Explicitly not in the plan + +- ComputeSDK as a dependency. +- File-hop or inbound wrapper HTTP for any new provider. +- Per-session VMs as the eventual default across all BYOC providers. Customer + Vercel Stage 1 uses them as a bounded MVP exception. +- PTY multiplexing in the first BYOC slice. +- Automatic multi-sandbox scheduling under load. +- Owner-scoped leak sweep (`listForOwner`) until a provider exposes it cheaply; residual leaks stay manual. + +--- + +## 9. Files that would change for Stage 1 (after call-home) + +These paths are relative to the call-home tree, not to today's production `AgentSandbox` layout. + +- `services/cloud-agent-next/src/sandbox-control/` — unchanged state machine; Vercel adapter behind the provider boundary, credentials from the org record. +- `packages/db/src/schema.ts` — encrypted org Vercel credentials (token, team, project) plus the org's `snapshotId` / `runtimeBuildId` from setup. +- `apps/web` org settings — token + team + project, then a setup action that copies/builds the snapshot into their project. +- Snapshot pipeline (`scripts/vercel-snapshot.ts`) — runnable with the org's token against the org's project as part of BYOC setup. + +Do **not** add `src/agent-sandbox/e2b/e2b-agent-sandbox.ts`. Do **not** add a second Vercel client. + +--- + +## 10. Residual risks specific to BYOC + +| Risk | Treatment | +|---|---| +| Building E2B against the old `AgentSandbox` seam | Wait for Stage 0. The seam is being deleted. | +| Customer key used to create instances we then lose track of | Create intent before create call; `observe` is three-valued; `unknown` pages a human. Owner sweep is later. | +| Sharing one Kilo across an org's chats | Owner boundary is mandatory. Measure crash blast radius before promising density. | +| Provider `observe` that only has running/not-running | Refuse that adapter until it can return `unknown`. A failed lookup must not become `stopped`. | +| Snapshot/template drift per vendor | One wrapper contract (call-home URL + credential + loopback-only listen). Vendor images only differ in how they boot. | +| Customer Vercel cannot use Kilo's snapshot ID | BYOC setup always copies or builds the snapshot into their project and stores that id. | +| Credential in snapshot or logs | Inject at instance create, never bake into the template. Header-only on the WebSocket. | + +--- + +## 11. Success criteria for BYOC + +The control-plane criteria in `CALL-HOME-CONTROL-PLANE.md` §17 plus: + +- BYOC setup copies or builds the wrapper snapshot into the customer's Vercel project; +- a customer-supplied Vercel token then starts a sandbox they pay for that only calls home; +- two sessions for that owner create distinct `ses-*` sandboxes for Stage 1; +- idle stop and replacement work without vendor-specific timeout branches; +- an enterprise runner can be described as "the same adapter, different `create` implementation" without a new protocol. diff --git a/docs/byoc-vercel-implementation-plan.md b/docs/byoc-vercel-implementation-plan.md new file mode 100644 index 00000000000..7e426c196e3 --- /dev/null +++ b/docs/byoc-vercel-implementation-plan.md @@ -0,0 +1,1129 @@ +# BYOC Stage 1 — Customer-paid Vercel + +## Current scope — 2026-10-13 + +Vercel is the only BYOC provider in the active stack. +Personal users and organization owners and admins manage it in **Cloud Agent settings → Compute**. +The former `/organizations/:id/cloud/compute` URL redirects to that settings tab. +Kubernetes, E2B, and the Integrations Cloud hub are outside this scope. +Platform-paid Cloudflare providers continue to serve other sessions. +Kilo-paid Vercel is retired. Only customer-bound Vercel compute can run. +Legacy Kilo-paid Vercel sessions fail closed and require a new session; existing remote resources need operator cleanup. + +The current implementation uses `SandboxControlV2` and the current control-plane wrapper. +It resolves customer credentials for provider operations and stores only their identity in the provider pin. +BYOC sessions omit Kilo compute billing and do not fall back to platform-paid compute. +Enrollment uses `BYOC_VERCEL_IDS`; trusted code-review sessions retain platform compute. +This replaces `BYOC_VERCEL_ORG_IDS`. Operators must move existing organization IDs into the new variable. +The new variable accepts user IDs and organization IDs; `*` enrolls both. +A credential has exactly one owner. Personal credentials never apply to organization sessions. + +The active BYOC chain is `chilly-willow` → `agile-spruce` → `twilight-nebula`. +`scarlet-cove` contains separate repository environment work. +The Kubernetes/E2B and personal/Integrations branch chains are detached from the active BYOC stack. +Their existing commits remain preserved; detachment does not rebase those branches onto `main`. +The personal Vercel changes are ported here without the Integrations UI. + +Focused tests cover provider selection, credential identity, billing exclusion, and validator lifecycle. +Live Vercel setup, session execution, credential removal, and runtime upgrade still need verification. +The sections below record the original implementation plan. Legacy file paths and runtime details may be outdated. + +**Date:** 2026-08-25 + +**Base:** `sticky-slider` at `989ae31740fef1128f05e9a221edfcd66547a43b` + +**Status:** Ready for implementation after the Slice 0 provider spike passes + +This plan adds an organization-scoped Vercel credential to the call-home +control plane. New sessions for an enrolled organization run in isolated +Vercel sandboxes paid for by that organization. + +The plan deliberately optimizes for a small, supportable MVP: + +- one credential and one Vercel project per Kilo organization; +- one Vercel sandbox per Cloud Agent session; +- no fallback from BYOC Vercel to Kilo-paid Vercel; +- immediate hard deletion when the organization removes its credential; +- no credential versions, soft revocation, key rotation, or background purge; +- a Durable Object for the long-running snapshot build, with durable + reconciliation around provider side effects; +- the current Kilo token behavior is accepted for Stage 1. + +Canonical runtime rules remain in `CALL-HOME-CONTROL-PLANE.md`. Background and +future architecture are in `docs/byoc-architecture-research.md`. + +--- + +## 1. Scope + +### 1.1 User-visible flow + +An organization owner or admin can: + +1. Open organization compute settings. +2. Enter a Vercel token, team ID, and project ID. +3. Start setup. Kilo validates access, builds one runtime snapshot in the + customer's project, boots a disposable child from it, and shows progress. +4. Kilo enables the organization in the server-side pilot allowlist after + setup reaches `ready`; there is no second customer-facing toggle. +5. Start new Cloud Agent sessions. Each new session receives a distinct + `ses-*` sandbox in the configured Vercel project. +6. Remove the credential after acknowledging that Kilo immediately loses + control of existing BYOC sandboxes and old sessions will stop working. + +The removal confirmation links to the configured Vercel project and tells the +user to open **Observability -> Sandboxes** so they can inspect or stop running +sandboxes first. After removal, when the project details are no longer +available, error and empty states link to the generic Vercel dashboard. + +### 1.2 Explicit non-goals + +- Shared owner sandboxes for BYOC. +- Personal-account BYOC. +- E2B, Modal, or additional keyed providers. +- Automatic provider fallback for a session bound to BYOC. +- Editing or replacing a credential in place. The flow is remove, then add. +- Credential history, soft revocation, grace periods, or restore. +- Encryption-key or Vercel-token rotation workflows. +- Discovering and stopping every Vercel sandbox during credential removal. +- Hiding `KILOCODE_TOKEN` from a customer's Vercel project administrators. +- A second retry system outside Durable Object alarms. +- Terminal/PTTY work beyond what call-home already provides. + +### 1.3 Existing building blocks to reuse + +Do not reimplement: + +| Capability | Current location | +|---|---| +| Provider contract | `services/cloud-agent-next/src/sandbox-control/provider.ts` | +| Kilo-paid Vercel adapter | `services/cloud-agent-next/src/sandbox-control/vercel-provider.ts` | +| Vercel REST client and `inspectByName` recovery | `services/cloud-agent-next/src/agent-sandbox/vercel/vercel-sandbox-rest-client.ts` | +| Snapshot build commands and validation sequence | `services/cloud-agent-next/scripts/vercel-snapshot.ts` | +| Physical lifecycle and deadlines | `services/cloud-agent-next/src/persistence/SandboxControl.ts` | +| Per-session runtime owner | `services/cloud-agent-next/src/sandbox-session/SandboxSession.ts` | +| Existing Cloud Agent server-to-server client | `apps/web/src/lib/cloud-agent-next/cloud-agent-client.ts` | +| Existing RSA envelope helpers | `@kilocode/encryption`, re-exported by web and Cloud Agent | +| Existing Kilo token containment path | `KILOCODE_TOKEN_CONTAINMENT_ORG_IDS` and the session materialization code | + +--- + +## 2. Required invariants + +These are acceptance criteria, not suggestions. + +### 2.1 Provider identity is closed and persistent + +A session records exactly one provider source at registration: + +```ts +type SandboxProviderBinding = + | { kind: 'cloudflare' } + | { kind: 'vercel'; source: { kind: 'platform' } } + | { + kind: 'vercel'; + source: { + kind: 'byoc'; + organizationId: string; + credentialId: string; + }; + }; +``` + +The BYOC pointer is required in the BYOC branch. It must not be modeled as an +optional property on `{ kind: 'vercel' }`, because that would allow a missing +pointer to fall through to the worker-global `VERCEL_TOKEN`. + +The binding is stored with session workspace metadata and passed to +`SandboxControl`. `SandboxControl` pins and compares the complete binding, not +only the provider name. + +Consequences: + +- Cloudflare, Kilo-paid Vercel, and BYOC Vercel are unambiguous. +- A missing/deleted BYOC row is a terminal configuration error. +- Existing sessions do not change provider when settings or enrollment change. +- BYOC never consumes Kilo's Vercel credentials by accident. + +### 2.2 BYOC is per session + +For a BYOC binding, `generateSandboxRoutingTarget` always returns the existing +isolated `ses-{hash(sessionId)}` shape. Do not add BYOC organizations to +`PER_SESSION_SANDBOX_ORG_IDS`; BYOC is intrinsically isolated and must not +alter their Cloudflare behavior. + +This avoids repinning a shared `SandboxControl` between provider sources. The +accepted MVP trade-off is one customer-paid VM and one cold start per open +session. + +### 2.3 Credential removal is immediate and fail closed + +Removal means: + +1. Authorize the caller as an owner/admin of the organization. +2. Hard-delete the organization's credential row in PostgreSQL. +3. Return success once that delete commits. + +No application component may retain the token or encrypted envelope outside +the PostgreSQL credential row: + +- do not store either in Durable Object storage; +- do not put either in session metadata; +- do not cache a decrypted provider adapter between operations; +- do not log request bodies, auth headers, envelopes, or plaintext tokens; +- keep plaintext only in operation-local scope and drop references when the + operation completes; JavaScript memory zeroization is not promised. + +After deletion, the next provider operation re-reads the credential, receives +`404`, and fails with `byoc_credential_missing`. It must not fall back. + +An operation already executing at deletion time may finish with credentials +already held in memory. Immediate cancellation of in-flight network calls is +not promised. Encrypted database backups follow the platform's normal backup +retention; Stage 1 does not introduce backup rewriting or cryptographic erasure. + +Existing provider-side resources remain in the customer's Vercel account. +Kilo cannot safely enumerate or stop them after deleting the token. The UI must +say this plainly and direct the user to Vercel. If the user needs the token +itself invalidated at Vercel, they must revoke it there. + +### 2.4 PostgreSQL is configuration and UI projection; the DO runs builds + +The web application owns authorization, credential creation/removal, and the +status shown in settings. The per-organization `VercelSnapshotBuild` Durable +Object owns execution and retries. + +Every build start has a random `buildGeneration`. Every status update from the +DO must use both `credentialId` and `buildGeneration`: + +```sql +UPDATE organization_vercel_compute_credentials +SET setup_status = $status, + setup_step = $step, + setup_error = $safe_error, + runtime_build_id = $runtime_build_id, + runtime_snapshot_id = $runtime_snapshot_id, + team_slug = $team_slug, + project_slug = $project_slug, + updated_at = now() +WHERE id = $credential_id + AND build_generation = $build_generation; +``` + +Zero updated rows means the credential was removed or a newer setup superseded +the caller. The DO stops advancing and forgets its local state. This fence is +mandatory at every projected transition. + +The web app does not bind or address the DO directly. It calls an authenticated +Cloud Agent internal endpoint; Cloud Agent resolves the DO stub. + +### 2.5 Persist intent before provider side effects + +Before every Vercel operation that can create or mutate a resource, persist a +stable operation intent in the DO. On retry, reconcile that same intent before +issuing the side effect again. + +At minimum: + +- builder create: persist deterministic sandbox name, create operation ID, and + expected inputs; call `inspectByName` first on every attempt; +- command execution: persist the command step before calling Vercel, and make + the command safe to repeat or verify its postcondition; +- snapshot create: persist the source session and the pre-operation snapshot + IDs before calling; after an uncertain result, list/inspect snapshots and + reconcile before deciding whether another POST is safe; +- validation sandbox create: use the same deterministic create pattern; +- stop: persist the target and retry/observe until terminal. + +Never infer that a timed-out create or snapshot did not happen. + +### 2.6 No rotation in Stage 1 + +Reuse the existing `AGENT_ENV_VARS_PUBLIC_KEY` in the web app and +`AGENT_ENV_VARS_PRIVATE_KEY` in Cloud Agent with +`encryptWithPublicKey`/`decryptWithPrivateKey`. This is the same trust boundary: +the web app may encrypt a secret that only the Cloud Agent worker may decrypt. + +Do not add key IDs, retired key slots, token versions, previous ciphertext, or +a rotation job. The operational constraint is explicit: do not rotate this RSA +pair while BYOC credential rows exist. A future rotation project must add a +real multi-key decrypt/read-rewrite path before changing the key. + +--- + +## 3. Data model and contracts + +### 3.1 PostgreSQL table + +Add `organization_vercel_compute_credentials` in +`packages/db/src/schema.ts`: + +| Column | Type | Notes | +|---|---|---| +| `id` | UUID | Primary key; generated for each add flow | +| `organization_id` | UUID | FK to `organizations.id`; unique; cascade delete | +| `token_encrypted` | JSONB | `EncryptedEnvelope`; never returned to the browser | +| `team_id` | text | Non-empty validated Vercel team identifier | +| `project_id` | text | Non-empty validated Vercel project identifier | +| `team_slug` | text nullable | Resolved during validation for the dashboard link | +| `project_slug` | text nullable | Resolved project name/slug for the dashboard link | +| `setup_status` | text enum/check | `pending`, `building`, `ready`, `failed` | +| `setup_step` | text nullable | Current safe UI step | +| `setup_error` | text nullable | Sanitized category/message only | +| `build_generation` | UUID | Fence for the active build | +| `runtime_build_id` | text nullable | Correlation value embedded in sandbox tags | +| `runtime_snapshot_id` | text nullable | Required for `ready` | +| `setup_started_at` | timestamp nullable | UI/operations | +| `setup_completed_at` | timestamp nullable | UI/operations | +| `created_at` | timestamp | Standard default | +| `updated_at` | timestamp | Standard default/update | + +Constraints: + +- one row per organization; +- `ready` requires the runtime snapshot, both dashboard slugs, and + `setup_completed_at`; +- non-`ready` rows are never eligible for new sessions; +- no `revoked_at`, token version, previous token, builder session ID, or purge + deadline columns. + +Follow `packages/db/AGENTS.md` and generate the migration through the repo's +normal Drizzle workflow. Do not hand-edit the migration journal. + +### 3.2 Web API + +Add +`apps/web/src/routers/organizations/organization-vercel-compute-router.ts` and +register it as `organizations.vercelCompute` in +`apps/web/src/routers/organizations/organization-router.ts`. Keep it separate +from the already-large Cloud Agent session router. + +Procedures: + +```ts +getStatus({ organizationId }) +add({ organizationId, token, teamId, projectId }) +retrySetup({ organizationId }) +remove({ organizationId }) +``` + +All procedures require organization owner/admin membership. + +`add`: + +1. Reject if a row already exists; the UI must use remove then add. +2. Validate field shape without logging input. +3. Encrypt the token. +4. Insert a `pending` row with fresh `id`, `runtimeBuildId`, and + `buildGeneration`. +5. Call Cloud Agent `startVercelSnapshotBuild`; the DO performs the Vercel + access check as its first step so provider logic remains in Cloud Agent. +6. If the call fails, keep the row and project `failed`; the user can retry or + remove it. + +`retrySetup` keeps the same credential row but creates a fresh +`runtimeBuildId` and `buildGeneration`, clears the snapshot ID/error, and starts +the DO again. It is setup retry, not credential rotation. + +`remove` only hard-deletes the row. It does not wait for Cloud Agent and cannot +fail because a provider/control-plane cleanup call is unavailable. The UI +already has the team/project identifiers needed for its pre-delete Vercel link. +The remove response never returns them, the token, or the envelope. + +### 3.3 Internal web endpoint for credential resolution + +Add: + +`apps/web/src/app/api/internal/byoc/vercel-credentials/[credentialId]/route.ts` + +Cloud Agent calls this endpoint with `x-internal-api-key` and both +`credentialId` and `organizationId`. The endpoint: + +- verifies the internal secret with the repository's constant-time helper; +- selects the row by both IDs; +- returns encrypted token, team/project, setup state, runtime snapshot ID, + `runtimeBuildId`, and `buildGeneration`; +- returns `404` immediately after deletion; +- sets `Cache-Control: no-store`; +- never decrypts or logs the token. + +Add a generation-fenced status-update endpoint beside it, or a single internal +route with explicit `GET` and `PATCH`. `PATCH` accepts only the finite setup +status/step contract and sanitized errors. It must not accept arbitrary column +updates. + +### 3.4 Cloud Agent internal endpoints + +Add authenticated Hono routes in `services/cloud-agent-next/src/server.ts`: + +```text +POST /internal/byoc/vercel-snapshot-build/start +``` + +It uses the existing `requireInternalApi` boundary. `start` accepts only +`organizationId`, `credentialId`, and `buildGeneration`. It forwards those to +the per-organization DO. + +Extend `apps/web/src/lib/cloud-agent-next/cloud-agent-client.ts` with typed +methods for these routes. Do not expose them in browser-side tRPC clients. + +### 3.5 Credential resolver in Cloud Agent + +Add a narrow server-only module, for example: + +`services/cloud-agent-next/src/byoc/vercel-credential-resolver.ts` + +It: + +1. Fetches the internal web endpoint using the raw + `KILOCODE_BACKEND_BASE_URL` Worker value and + `await env.INTERNAL_API_SECRET_PROD.get()`, matching the existing outbound + internal-call pattern. Do not use the sandbox-rewritten backend URL. +2. Uses `cache: 'no-store'`. +3. Validates the response with Zod. +4. Decrypts with `AGENT_ENV_VARS_PRIVATE_KEY` only immediately before an + operation. +5. Returns a short-lived `VercelSandboxRuntimeConfig` value to the caller. +6. Converts `404` into a non-retryable `ByocCredentialMissingError`. + +The resolver has no module-level cache and no DO-storage cache. + +### 3.6 Session metadata + +Update the shared metadata schema and all serialization boundaries that +currently carry `sandboxProvider` so they carry `SandboxProviderBinding`. +Relevant current paths include: + +- `services/cloud-agent-next/src/persistence/session-metadata.ts` +- `services/cloud-agent-next/src/session/session-registration.ts` +- `services/cloud-agent-next/src/session-service.ts` +- `services/cloud-agent-next/src/persistence/CloudAgentSession.ts` +- `services/cloud-agent-next/src/persistence/SandboxControl.ts` +- `services/cloud-agent-next/src/sandbox-control/stub.ts` + +Use a schema-version/default compatibility path only for metadata already in +production: + +- legacy `cloudflare` maps to `{ kind: 'cloudflare' }`; +- legacy `vercel` maps to `{ kind: 'vercel', source: { kind: 'platform' } }`; +- no legacy value may synthesize a BYOC pointer. + +The BYOC metadata branch stores only the binding pointer and existing opaque +runtime correlation fields needed by the adapter. It never stores the token, +encrypted envelope, team/project configuration, or source snapshot. Preserve +the existing platform-Vercel `providerRuntime` compatibility fields; removing +them is unrelated to this feature. + +--- + +## 4. Runtime provider resolution + +### 4.1 Enrollment and registration + +Add `BYOC_VERCEL_IDS` as the BYOC launch kill switch. Keep +`VERCEL_SANDBOX_ORG_IDS` exclusively for the existing Kilo-paid Vercel path so +the two sources cannot be confused. At session registration: + +1. If the session has no organization, use the existing provider selection. +2. If the organization is not in `BYOC_VERCEL_IDS`, use the existing + provider selection, including `VERCEL_SANDBOX_ORG_IDS`. +3. If it is in `BYOC_VERCEL_IDS`, fetch its BYOC status through the + internal credential endpoint. +4. If the row is `ready`, persist a BYOC provider binding and generate a + per-session `ses-*` sandbox ID. +5. If the row is absent or not ready, fail registration with an actionable + `byoc_vercel_not_ready` error. Do not silently use Cloudflare or Kilo-paid + Vercel for an organization explicitly enrolled in BYOC. + +This makes launch behavior deterministic. Enabling or disabling the allowlist +only affects new sessions. + +### 4.2 Adapter construction + +Refactor `SandboxControl.createProviderAdapter` into an async operation-scoped +factory: + +```ts +async function createProviderAdapterForOperation( + env: PersistenceEnv, + binding: SandboxProviderBinding +): Promise +``` + +- Cloudflare uses the existing binding/adapter. +- Platform Vercel uses `parseVercelSandboxRuntimeConfig(env)`. +- BYOC Vercel resolves and decrypts the row for that operation, verifies the + credential ID and organization, and creates the existing Vercel adapter with + that config. + +Remove `private provider`/promise caching from `SandboxControl`. Each durable +operation (`create`, `observe`, `stop`, `ensureLeaseAtLeast`, `logs`) obtains an +adapter, invokes one logical provider operation, then releases it. This extra +internal request is intentional: credential deletion must take effect on the +next operation. + +The closed binding switch must be exhaustive. Never implement: + +```ts +if (binding.byoc) return byocAdapter; +return platformVercelAdapter; +``` + +because a malformed BYOC record would become a billing/security fallback. + +### 4.3 Removal behavior for existing sessions + +After removal: + +- a connected wrapper may continue its already-established WebSocket until a + lifecycle action requires credentials or the connection closes; +- new create/observe/lease/stop/log operations fail + `byoc_credential_missing`; +- queued messages terminalize with clear copy instead of retrying forever; +- Kilo cannot guarantee stop or cleanup of the Vercel VM; +- the session UI points the user to Vercel and recommends starting a new + session after reconfiguring compute. + +This loss of control is accepted product behavior and must be covered by tests +and user-facing warnings. + +--- + +## 5. Snapshot-build Durable Object + +### 5.1 Ownership and bindings + +Add `VercelSnapshotBuild`, addressed by organization ID. There is one active +build per organization. Register the class and binding in: + +- `services/cloud-agent-next/src/persistence/VercelSnapshotBuild.ts` +- `services/cloud-agent-next/src/index.ts` +- `services/cloud-agent-next/src/types.ts` +- `services/cloud-agent-next/src/persistence/types.ts` +- `services/cloud-agent-next/wrangler.jsonc` +- `services/cloud-agent-next/worker-configuration.d.ts` + +Add the state table to `services/cloud-agent-next/src/db/sqlite-schema.ts`, run +the service's Drizzle generation workflow, and commit the generated SQL, +`drizzle/migrations.js`, and metadata. Add `VercelSnapshotBuild` as the next +`new_sqlite_classes` Wrangler migration (`v11` at this base). Its constructor +uses `drizzle-orm/durable-sqlite` and runs `migrate(db, migrations)` inside +`blockConcurrencyWhile`; no external I/O belongs in that block. Use only +Drizzle query-builder operations for SQLite access. + +Store a single state-machine row plus bounded operation-intent fields. Do not +store credentials or envelopes. Keep the DO class to RPC/alarm orchestration; +put state transitions and Vercel build operations in plain-function modules +under `src/persistence/vercel-snapshot-build/`. + +Export `getVercelSnapshotBuildStub(env, organizationId)` beside the class and +route every caller through it. The helper uses deterministic `getByName`. +Worker-to-DO calls use the service's `src/utils/do-retry.ts` wrapper so a +retryable DO error obtains a fresh stub instead of reusing a broken one. + +Public RPC surface: + +```ts +start(input: { + organizationId: string; + credentialId: string; + buildGeneration: string; +}): Promise + +alarm(): Promise +``` + +`start` is idempotent for the same generation. It reads the credential first, +then re-reads local state after that external I/O before writing. A newer +generation replaces old local state only when the row's credential ID and +build generation match the request. `ready`, `failed`, stale-generation, and +missing-credential terminal paths erase the DO's local build state after +projecting their final safe status where applicable. + +### 5.2 Runtime artifacts + +The DO cannot read the operator's local filesystem. For the MVP, ship the two +existing generated artifacts with the Worker instead of adding an artifact +registry: + +- `wrapper/dist/wrapper.js`; +- `wrapper/dist/control-wrapper.js`. + +Add `services/cloud-agent-next/src/byoc/vercel-runtime-artifacts.ts` with static +Data-module imports for those files and a narrow accessor returning bytes plus +hashes/version. Add exact `Data` rules to `wrangler.jsonc`, an ambient module +type if TypeScript requires it, and a `predeploy` script that runs the existing +`build:wrapper`. Local dev already builds the wrapper through `predev`. + +The current generated artifacts are approximately 260 KiB and 480 KiB. This is +small enough to try the direct approach, but Slice 4 must run a Wrangler +`deploy --dry-run` and record compressed/uncompressed Worker size. If it would +breach the account's Worker limit or materially harm startup, stop and move the +same versioned artifacts to the already-bound R2 bucket; do not add R2 and +embedded delivery simultaneously. + +### 5.3 Build state machine + +Use explicit, finite steps: + +```text +validating_access + -> create_builder + -> install_system_dependencies + -> install_node_dependencies + -> upload_runtime_artifacts + -> verify_runtime_artifacts + -> snapshot_builder + -> create_validator + -> launch_validator_wrapper + -> verify_validator_call_home + -> stop_validator + -> confirm_terminal + -> ready +``` + +Every step: + +1. Reads/fences the current credential row. +2. Persists the next operation intent locally before external work. +3. Performs at most one bounded provider action or one bounded command. +4. Persists the result. +5. Projects safe status to PostgreSQL with the generation fence. +6. Arms the next alarm. + +Do not run the full `scripts/vercel-snapshot.ts` sequence in one request. Extract +and reuse its commands, validation rules, and REST primitives, but split them +into alarm-sized operations. + +Alarms execute at least once and Cloudflare's automatic exception retries are +finite. The handler therefore catches classified transient failures, persists +its retry count/next time, and explicitly sets the next alarm. Deterministic or +exhausted failures project `failed` and clear local state. Every replay starts +from persisted state; class fields are caches only. + +### 5.4 Builder-create recovery + +Vercel returns the exact session ID only in the create response. Therefore a +lost response must be recoverable by name: + +- deterministic name: derived from credential ID and runtime build ID; +- deterministic create operation ID: persisted before POST; +- expected runtime/snapshot/tag inputs: persisted before POST; +- on every attempt, call `inspectByName` first; +- if found and correlation succeeds, persist the returned session ID; +- if not found, issue create once; +- if inspection is inconclusive, retry inspection; do not create. + +The existing REST client's runtime create path requires a source snapshot, but +the builder starts from a plain runtime. Extend its create input as a closed +union of `runtime` and `snapshot` sources, and make `inspectByName` correlate +the selected source. Reuse its existing name/tag recovery; do not add an +optional snapshot field that can silently select the wrong path. Do not rely on +in-memory `trackedSessions` from the CLI script. + +### 5.5 Command recovery + +Each install/upload/verify command must be safe to retry. Prefer commands with +an observable postcondition, such as a marker containing the runtime build ID +written only after successful completion. On alarm replay: + +- if the marker/postcondition exists, advance; +- if not, run the same command again; +- never advance solely because the command request was sent. + +Keep commands coarse enough to avoid unnecessary state but small enough to +finish within provider and Worker limits. There is no separate workflow engine. + +### 5.6 Snapshot recovery + +Before snapshot POST: + +1. Persist the source session ID and the current set of snapshot IDs. +2. Mark the snapshot operation `requested`. +3. Call create snapshot. + +On a definite response, validate `sourceSessionId`, status, and snapshot ID, +then persist it. On timeout/transport ambiguity: + +1. List snapshots created since the persisted baseline. +2. Inspect candidates and correlate them to the source session. +3. If exactly one matches, adopt it. +4. If none match and the source session is still active, retry after another + bounded reconciliation pass. +5. If multiple match or provider state cannot prove absence, mark setup failed + with `snapshot_result_ambiguous`; do not blindly POST another snapshot. + +The implementation spike must prove Vercel's list/inspect responses contain +enough information to perform step 2. If they do not, Stage 1 is blocked until +we choose a safe provider-supported correlation mechanism. This is the only +intentional pre-implementation gate. + +Vercel snapshot creation stops the source sandbox. Treat a separate stop call +as terminal confirmation/reconciliation, not an unconditional next side effect. + +### 5.7 Failure and cancellation + +Retry only classified transient provider/network failures with a bounded +backoff. Invalid token, forbidden team/project, missing credential, failed +correlation, and deterministic command failures become `failed` immediately. + +Persist only sanitized error categories/messages to PostgreSQL. Logs may carry +organization ID, credential ID, build generation, operation ID, provider +status, and HTTP status; never token, envelope, auth header, or raw provider +body that could contain secrets. + +If a credential disappears during a build, the next armed alarm receives +`404`, cancels further work, and deletes local state without trying to stop +provider resources. This is deliberate: removal has already deleted the only +authority needed to control them. + +--- + +## 6. Existing lifecycle fixes required before enrollment + +Current `SandboxControl` has semantic deadlines (`idleStop`, `stopAttempt`, and +`reconciliation`), but the customer-billing safety behavior must be verified +and fixed before any BYOC org is enrolled. + +### 6.1 Stop retry + +When `provider.stop` returns `retryable`, the `stopAttempt` deadline must call +`provider.stop` again for the same persisted provider reference. Recording an +attempt and rearming without issuing stop is not sufficient. + +Required transition: + +```text +stop requested + -> stop(ref) + -> retryable + -> persist attempt + arm stopAttempt + -> stop(ref) again + -> terminal: clear provider state + -> retry budget exhausted: keep reconciliation armed +``` + +### 6.2 Reconciliation + +On `reconciliation`, observe the persisted reference: + +- `terminal`: clear provider state and cancel stop deadlines; +- `active` with no attached work: re-enter the stop transition; +- `active` with work: preserve it and rearm reconciliation; +- `unknown`: preserve state and rearm reconciliation. + +This must work for Cloudflare, platform Vercel, and BYOC Vercel. For BYOC after +credential deletion, observation fails closed and telemetry identifies the +uncontrollable resource; it must not erase the reference as if terminal. + +### 6.3 Queue terminalization + +`ByocCredentialMissingError`, invalid credential, and non-retryable provider +capacity/configuration errors must terminalize the affected message/session +with actionable failure data. They must not enter the generic five-second queue +retry loop indefinitely. + +--- + +## 7. UI requirements + +Add organization compute settings at +`apps/web/src/app/(app)/organizations/[id]/cloud/compute/`, alongside the +existing organization Cloud pages. Follow `apps/web/AGENTS.md`, `DESIGN.md`, +and the `kilo-design-cloud` patterns during implementation. + +### 7.1 Add/setup + +- Inputs: Vercel token, team ID, project ID. +- Token input is write-only; never redisplay it. +- Explain that setup creates temporary sandboxes and snapshots in the selected + project and can incur Vercel charges. +- Show finite setup steps from the PostgreSQL projection. +- `failed` offers Retry setup and Remove credentials. +- `ready` shows team/project and snapshot readiness. Setup resolves + team/project slugs while validating access; IDs must never be interpolated as + dashboard path slugs. + +### 7.2 Remove confirmation + +The destructive confirmation must say, in equivalent plain language: + +> Removing these credentials deletes Kilo's copy immediately. Kilo will no +> longer be able to start, inspect, extend, or stop your Vercel sandboxes. +> Existing Cloud Agent sessions using this project will stop working. Review +> and stop running sandboxes in Vercel before continuing. + +Include `https://vercel.com/{teamSlug}/{projectSlug}` and tell the user to open +**Observability -> Sandboxes**. Require an explicit confirmation action; do not +require typing the token or project name. + +After success: + +- clear all credential/setup details from the page; +- show a generic Vercel dashboard link for unmanaged running sandboxes; +- explain that the user may revoke the token in Vercel if it should no longer + be valid there; +- do not offer restore or undo. + +### 7.3 Session errors + +Map at least: + +- `byoc_vercel_not_ready` -> finish setup or contact an org admin; +- `byoc_credential_missing` -> credentials were removed; inspect Vercel and + start a new session after reconfiguration; +- `byoc_vercel_capacity` -> check Vercel concurrency/spend limits; +- `byoc_vercel_forbidden` -> check token/team/project access. + +Do not show raw Vercel responses or identifiers that are not needed for support. + +--- + +## 8. Kilo token exposure + +For Stage 1, the wrapper receives the current `KILOCODE_TOKEN` behavior. A +customer who administers the Vercel project may be able to inspect process +environment or VM state and obtain it. This is accepted for the MVP and must be +documented in the enrollment risk note. + +Do not build a new containment design in this work. The follow-up is to adapt +the existing Cloudflare containment/capability path represented by +`KILOCODE_TOKEN_CONTAINMENT_ORG_IDS`, rather than create a Vercel-specific +second mechanism. + +--- + +## 9. Implementation slices + +Each slice should be a small reviewable commit/PR and leave default production +traffic unchanged. + +### Slice 0 — Provider spike and contract proof + +Goal: remove the only provider-contract uncertainty before schema/UI work. + +Tasks: + +- In an isolated Vercel test project, create a sandbox with deterministic name + and tags, lose/ignore the response, and recover it through `inspectByName`. +- Create a snapshot, then prove list/inspect can correlate it to the source + session after response loss. +- Confirm snapshot creation's source-session stop behavior. +- Record sanitized response shapes and the chosen correlation algorithm in a + focused test/fixture or a short appendix to this document. + +Gate: do not start later slices if snapshot response-loss cannot be reconciled +without risking duplicate snapshots. + +### Slice 1 — Lifecycle billing safety + +Files: + +- `services/cloud-agent-next/src/persistence/SandboxControl.ts` +- `services/cloud-agent-next/src/sandbox-control/reconciliation.ts` +- focused tests under `src/sandbox-control/recovery/` and persistence tests + +Deliver: + +- real stop reissue on `stopAttempt`; +- active/no-work reconciliation re-enters stop; +- non-retryable configuration errors terminalize queues. + +No BYOC schema or UI in this slice. + +### Slice 2 — Credential persistence and removal + +Files: + +- `packages/db/src/schema.ts` +- generated `packages/db/src/migrations/*` +- `apps/web/src/routers/organizations/organization-vercel-compute-router.ts` +- `apps/web/src/routers/organizations/organization-router.ts` +- `apps/web/src/app/api/internal/byoc/vercel-credentials/[credentialId]/route.ts` +- web configuration/encryption tests + +Deliver: + +- one hard-deletable encrypted row per organization; +- owner/admin authorization; +- add/status/retry/remove contracts; +- internal no-store GET and generation-fenced PATCH; +- no version/revocation/rotation machinery. + +### Slice 3 — Closed provider binding and resolver + +Files: + +- session metadata/registration files listed in §3.6; +- `services/cloud-agent-next/src/byoc/vercel-credential-resolver.ts`; +- `services/cloud-agent-next/src/persistence/SandboxControl.ts`; +- `services/cloud-agent-next/src/sandbox-control/vercel-provider.ts` as needed; +- `services/cloud-agent-next/src/types.ts` and persistence types. + +Deliver: + +- discriminated provider source union with legacy read compatibility; +- operation-scoped credential lookup/decrypt; +- no provider cache; +- hard fail on missing BYOC credentials; +- existing Cloudflare and platform Vercel paths unchanged. + +### Slice 4 — Snapshot-build DO and internal control API + +Files: + +- `services/cloud-agent-next/src/persistence/VercelSnapshotBuild.ts` +- `services/cloud-agent-next/src/persistence/vercel-snapshot-build/*` +- `services/cloud-agent-next/src/db/sqlite-schema.ts` and generated + `services/cloud-agent-next/drizzle/*` artifacts; +- `services/cloud-agent-next/src/byoc/vercel-runtime-artifacts.ts` +- `services/cloud-agent-next/src/server.ts` +- `services/cloud-agent-next/src/index.ts` +- `services/cloud-agent-next/src/types.ts` +- `services/cloud-agent-next/src/persistence/types.ts` +- `services/cloud-agent-next/wrangler.jsonc` +- `services/cloud-agent-next/worker-configuration.d.ts` +- `services/cloud-agent-next/package.json` +- `apps/web/src/lib/cloud-agent-next/cloud-agent-client.ts` +- extracted/reused Vercel snapshot primitives + +Deliver the state machine and reconciliation rules from §5. The CLI snapshot +script may be refactored to call shared primitives, but the DO must not spawn or +depend on the CLI script. + +### Slice 5 — Routing and enrollment + +Files: + +- provider selection and routing helpers in `services/cloud-agent-next/src/`; +- `services/cloud-agent-next/src/session/session-registration.ts`; +- relevant router/session preparation tests; +- `services/cloud-agent-next/wrangler.jsonc`, `services/cloud-agent-next/src/types.ts`, + generated worker configuration, `.dev.vars.example`, and `ENVIRONMENT.md` for + the new `BYOC_VERCEL_IDS` kill switch. + +Deliver: + +- ready + allowlisted -> BYOC binding + `ses-*`; +- allowlisted but absent/not-ready -> explicit failure; +- existing sessions remain pinned; +- personal accounts and non-allowlisted orgs preserve current behavior. + +### Slice 6 — Organization settings UI + +Deliver the add/progress/retry/removal flow and exact warning behavior in §7. +UI polling may use the existing tRPC query invalidation/polling pattern; do not +add WebSockets or another event system for setup progress. + +### Slice 7 — End-to-end pilot + +- Run setup against a dedicated customer-like Vercel team/project. +- Start two sessions and prove they create two distinct `ses-*` sandboxes. +- Exercise prompt dispatch, reconnect, idle stop, stop retry, and reconciliation. +- Remove the credential while a session exists and verify the DB row disappears, + future control fails closed, the user gets the warning/error path, and the + Vercel dashboard still exposes the unmanaged sandbox. +- Inspect logs for token/envelope/header leakage. +- Enroll one design-partner organization only after all rollout gates pass. + +--- + +## 10. Test and verification matrix + +### 10.1 Required focused tests + +Credential/API tests: + +- only org owner/admin can read status, add, retry, or remove; +- add rejects a second row; +- plaintext token is never returned or logged; +- remove hard-deletes the row; +- internal GET returns `404` after removal and `Cache-Control: no-store`; +- stale generation PATCH updates zero rows; +- `ready` cannot be projected without the runtime snapshot and dashboard slugs. + +Provider-binding tests: + +- each union member selects exactly one adapter source; +- malformed/missing BYOC pointer is rejected; +- deleted row never falls back to platform Vercel; +- legacy metadata maps only to Cloudflare/platform Vercel; +- BYOC adapter is resolved again on each operation; +- session routing for BYOC is always `ses-*`. + +Build-state tests: + +- same-generation `start` is idempotent; +- stale alarm/generation cannot overwrite a newer setup; +- create response loss is recovered by `inspectByName`; +- inconclusive inspection does not issue another create; +- command replay checks its postcondition; +- snapshot response loss adopts exactly one correlated snapshot; +- ambiguous snapshot state fails without duplicate POST; +- missing credential causes local state to be forgotten; +- persisted/logged state contains no token/envelope. +- Workers-runtime alarm tests use `runDurableObjectAlarm` and verify replay from + SQLite after object re-instantiation; +- retryable Worker-to-DO calls create a fresh stub through `withDORetry`. + +Lifecycle tests: + +- retryable stop calls `stop` again; +- active/no-work reconciliation restarts stop; +- unknown observation preserves provider reference and rearms; +- missing BYOC credential terminalizes work without clearing the provider as + successfully stopped. + +### 10.2 Narrow verification commands + +Before implementation, read the root and affected package manifests and use +their exact scripts. At minimum, expect package-filtered equivalents of: + +```sh +pnpm --filter cloud-agent-next typecheck +pnpm --filter cloud-agent-next test +pnpm --filter cloud-agent-next test:integration +pnpm --filter web typecheck +pnpm --filter web test +pnpm --filter @kilocode/db typecheck +pnpm lint +``` + +Use the `repository-verification` skill during implementation to choose the +actual supported commands. Root `pnpm test` is not proof that the Cloud Agent +service suite ran. + +### 10.3 Security review checklist + +- Search logs/tests/fixtures for the test token and encrypted envelope. +- Verify Sentry extras never receive input objects containing credentials. +- Verify provider errors are sanitized before persistence or UI display. +- Verify internal routes reject missing/wrong shared secrets. +- Verify every credential read checks both credential and organization IDs. +- Verify every build projection checks credential ID and generation. +- Verify no DO/session metadata contains ciphertext or plaintext. +- Verify deletion does not call Cloud Agent or Vercel and commits independently. + +--- + +## 11. Deployment, rollout, and rollback + +### 11.1 Deployment order + +1. Land Slice 1 lifecycle fixes and observe existing providers. +2. Deploy DB migration and web internal endpoints. +3. Deploy Cloud Agent resolver, provider-binding compatibility, DO class, + migration, embedded runtime artifacts, and internal build endpoints. The + deployment must use the package script so `predeploy` rebuilds the artifacts. +4. Deploy settings API/UI with setup available but enrollment allowlist empty. +5. Complete an internal Vercel setup and E2E run. +6. Add one design-partner organization to `BYOC_VERCEL_IDS`. +7. Expand only after stop/reconciliation and Vercel spend telemetry are clean. + +Durable Object class/binding/migration deployment must be validated in a +non-production environment before the production Worker deploy. Do not expose +the UI action until both directions of the web/Worker internal API are live. + +### 11.2 Rollout gates + +All must be true: + +- Slice 0 proves create and snapshot ambiguity recovery. +- No unresolved stop/reconciliation correctness failures. +- Credential deletion and stale-generation tests pass. +- Two-session per-session E2E passes. +- Token leak audit is clean. +- Wrangler dry-run confirms the Worker bundle remains within its account limit. +- Alerts/metrics distinguish platform Vercel from BYOC without token/team + values. +- Support runbook explains removed credentials and unmanaged Vercel sandboxes. +- Product/security explicitly accept MVP Kilo token exposure. + +A rejected create request, including HTTP 429, clears its create intent and can retry. +Transport failures, HTTP 408/409, and server errors can leave an unknown provider outcome. +Cleanup retains those intents until provider inspection finds the resource and confirms its removal. +If inspection cannot resolve an intent, the owner can remove the connection with explicit cleanup-failure acknowledgement. +This action releases Kilo ownership; the owner must check and remove remaining resources in Vercel. +Elapsed time alone does not prove that a provider create request failed. + +### 11.3 Kill switch and rollback + +Primary kill switch: remove organizations from `BYOC_VERCEL_IDS`. This +stops new BYOC session registration but does not alter existing sessions or +delete credentials. + +Rollback rules: + +- Preserve metadata compatibility code while any BYOC session can exist. +- Do not roll back the DB migration while rows exist. +- Do not automatically delete customer credentials during a software rollback. +- Do not move existing BYOC sessions to Cloudflare/platform Vercel. +- If snapshot setup is broken, disable setup starts and enrollment; keep status + and removal available. +- Credential removal remains irreversible from Kilo's side. A rollback cannot + restore control of an already-unmanaged Vercel sandbox. + +--- + +## 12. Observability and operations + +Structured events should include safe identifiers only: + +- `byoc_vercel_setup_started` +- `byoc_vercel_setup_step` +- `byoc_vercel_setup_failed` +- `byoc_vercel_setup_ready` +- `byoc_vercel_credential_removed` +- `byoc_vercel_credential_missing` +- `byoc_vercel_resource_uncontrollable` +- `byoc_vercel_snapshot_ambiguous` + +Safe fields: organization ID, credential ID, build generation, operation ID, +sandbox ID, provider status, attempt count, duration, and sanitized error code. + +Never include: token, encrypted envelope, Authorization header, internal API +secret, wrapper credential, Kilo token, or raw provider response bodies. + +Operational dashboards should separate BYOC from platform Vercel for creates, +active resources, stops, reconciliation age, and failures. This is a source tag, +not the customer team/project value. + +--- + +## 13. Residual risks accepted for Stage 1 + +1. Removing credentials immediately makes existing sessions uncontrollable; + running resources may continue billing until the customer stops them in + Vercel. +2. In-flight provider operations may finish after deletion. +3. Encrypted DB backups retain deleted ciphertext according to normal backup + retention. +4. The RSA key pair cannot be rotated while BYOC rows exist. +5. Customer Vercel administrators may obtain `KILOCODE_TOKEN` from sandbox + state. +6. Per-session VMs increase customer concurrency, spend, and cold starts. +7. Vercel API behavior is an external dependency; snapshot ambiguity recovery + must be proven by Slice 0 before implementation proceeds. + +These are explicit MVP limitations, not hidden follow-up machinery. + +--- + +## 14. Definition of done + +The feature is ready to enroll one design partner when: + +- the Slice 0 provider gate passes; +- all required invariants and focused tests pass; +- setup reaches `ready` through the Durable Object after restart/replay tests; +- new enrolled sessions use the customer's Vercel project and unique `ses-*` + sandboxes; +- existing sessions retain their original provider binding; +- credential removal hard-deletes the active DB row, leaves no credential copy + in application state, and causes subsequent control to fail closed; +- removal and session UI direct the customer to Vercel for running resources; +- stop retry and reconciliation are proven for customer-paid resources; +- no secrets appear in logs, telemetry, metadata, DO storage, or responses; +- rollout/rollback runbooks are written and the allowlist is empty by default. + +No credential rotation or Kilo-token containment work is required for Stage 1. diff --git a/packages/db/src/migrations/0302_typical_squirrel_girl.sql b/packages/db/src/migrations/0302_typical_squirrel_girl.sql new file mode 100644 index 00000000000..ef548390f06 --- /dev/null +++ b/packages/db/src/migrations/0302_typical_squirrel_girl.sql @@ -0,0 +1,37 @@ +CREATE TABLE "organization_vercel_compute_credentials" ( + "id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "organization_id" uuid, + "user_id" text, + "token_encrypted" jsonb NOT NULL, + "token_scope" text DEFAULT 'team' NOT NULL, + "team_id" text NOT NULL, + "project_id" text NOT NULL, + "team_slug" text, + "project_slug" text, + "setup_status" text DEFAULT 'pending' NOT NULL, + "setup_step" text, + "setup_error" text, + "build_generation" uuid DEFAULT pg_catalog.gen_random_uuid() NOT NULL, + "runtime_build_id" text, + "runtime_snapshot_id" text, + "runtime_wrapper_version" text, + "runtime_released_at" text, + "runtime_digest" text, + "upgrade_status" text, + "upgrade_step" text, + "upgrade_error" text, + "setup_started_at" timestamp with time zone, + "setup_completed_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "organization_vercel_compute_credentials_owner_check" CHECK (("organization_vercel_compute_credentials"."organization_id" IS NOT NULL AND "organization_vercel_compute_credentials"."user_id" IS NULL) OR ("organization_vercel_compute_credentials"."organization_id" IS NULL AND "organization_vercel_compute_credentials"."user_id" IS NOT NULL)), + CONSTRAINT "organization_vercel_compute_credentials_ids_non_empty" CHECK (length(trim("organization_vercel_compute_credentials"."team_id")) > 0 AND length(trim("organization_vercel_compute_credentials"."project_id")) > 0), + CONSTRAINT "organization_vercel_compute_credentials_token_scope_check" CHECK ("organization_vercel_compute_credentials"."token_scope" IN ('team', 'project')), + CONSTRAINT "organization_vercel_compute_credentials_status_check" CHECK ("organization_vercel_compute_credentials"."setup_status" IN ('pending', 'building', 'ready', 'failed')), + CONSTRAINT "organization_vercel_compute_credentials_ready_fields_check" CHECK ("organization_vercel_compute_credentials"."setup_status" <> 'ready' OR ("organization_vercel_compute_credentials"."runtime_snapshot_id" IS NOT NULL AND "organization_vercel_compute_credentials"."project_slug" IS NOT NULL AND "organization_vercel_compute_credentials"."setup_completed_at" IS NOT NULL)), + CONSTRAINT "organization_vercel_compute_credentials_upgrade_status_check" CHECK ("organization_vercel_compute_credentials"."upgrade_status" IS NULL OR "organization_vercel_compute_credentials"."upgrade_status" IN ('pending', 'building', 'failed')) +); +--> statement-breakpoint +ALTER TABLE "organization_vercel_compute_credentials" ADD CONSTRAINT "organization_vercel_compute_credentials_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_organization_vercel_compute_credentials_organization" ON "organization_vercel_compute_credentials" USING btree ("organization_id") WHERE "organization_vercel_compute_credentials"."organization_id" IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "UQ_organization_vercel_compute_credentials_user" ON "organization_vercel_compute_credentials" USING btree ("user_id") WHERE "organization_vercel_compute_credentials"."user_id" IS NOT NULL; \ No newline at end of file diff --git a/packages/db/src/migrations/meta/0302_snapshot.json b/packages/db/src/migrations/meta/0302_snapshot.json new file mode 100644 index 00000000000..17d5a35a680 --- /dev/null +++ b/packages/db/src/migrations/meta/0302_snapshot.json @@ -0,0 +1,39612 @@ +{ + "id": "baf955c0-049d-489c-9e25-ad1a2d597e83", + "prevId": "ff5b8190-a865-47ea-9c73-8120c8d9963a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.admin_action_audit_logs": { + "name": "admin_action_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_admin_action_audit_logs_created_at": { + "name": "IDX_admin_action_audit_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_admin_action_audit_logs_action_created_at": { + "name": "IDX_admin_action_audit_logs_action_created_at", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_admin_action_audit_logs_actor_id_created_at": { + "name": "IDX_admin_action_audit_logs_actor_id_created_at", + "columns": [ + { + "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": {} + }, + "IDX_admin_action_audit_logs_target_created_at": { + "name": "IDX_admin_action_audit_logs_target_created_at", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_briefing_context_preferences": { + "name": "agent_briefing_context_preferences", + "schema": "", + "columns": { + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_key": { + "name": "context_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ai_enabled": { + "name": "ai_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "max_duration_seconds": { + "name": "max_duration_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 600 + }, + "max_spend_microdollars": { + "name": "max_spend_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 2000000 + } + }, + "indexes": {}, + "foreignKeys": { + "agent_briefing_context_preferences_kilo_user_id_kilocode_users_id_fk": { + "name": "agent_briefing_context_preferences_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "agent_briefing_context_preferences", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_briefing_context_preferences_organization_id_organizations_id_fk": { + "name": "agent_briefing_context_preferences_organization_id_organizations_id_fk", + "tableFrom": "agent_briefing_context_preferences", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "agent_briefing_context_preferences_kilo_user_id_context_key_pk": { + "name": "agent_briefing_context_preferences_kilo_user_id_context_key_pk", + "columns": [ + "kilo_user_id", + "context_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_briefing_context_limits": { + "name": "agent_briefing_context_limits", + "value": "\"agent_briefing_context_preferences\".\"max_duration_seconds\" BETWEEN 1 AND 600 AND \"agent_briefing_context_preferences\".\"max_spend_microdollars\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.agent_briefing_events": { + "name": "agent_briefing_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_briefing_events_user_window": { + "name": "agent_briefing_events_user_window", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_briefing_events_session_window": { + "name": "agent_briefing_events_session_window", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_briefing_events_session_fk": { + "name": "agent_briefing_events_session_fk", + "tableFrom": "agent_briefing_events", + "tableTo": "cli_sessions_v2", + "columnsFrom": [ + "session_id", + "kilo_user_id" + ], + "columnsTo": [ + "session_id", + "kilo_user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_briefing_preferences": { + "name": "agent_briefing_preferences", + "schema": "", + "columns": { + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "night_start": { + "name": "night_start", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'22:00'" + }, + "morning_start": { + "name": "morning_start", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'08:00'" + }, + "morning_end": { + "name": "morning_end", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'12:00'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'en'" + }, + "notifications_enabled": { + "name": "notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "next_run_at": { + "name": "next_run_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": { + "agent_briefing_preferences_due": { + "name": "agent_briefing_preferences_due", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_briefing_preferences_kilo_user_id_kilocode_users_id_fk": { + "name": "agent_briefing_preferences_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "agent_briefing_preferences", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_briefing_reports": { + "name": "agent_briefing_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_key": { + "name": "context_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence_recorded_before": { + "name": "evidence_recorded_before", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "local_date": { + "name": "local_date", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "night_start_at": { + "name": "night_start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cutoff_at": { + "name": "cutoff_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "morning_end_at": { + "name": "morning_end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_refresh_at": { + "name": "next_refresh_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "next_charge_check_at": { + "name": "next_charge_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "evidence_hash": { + "name": "evidence_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "facts": { + "name": "facts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "generation_started_at": { + "name": "generation_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "generation_finished_at": { + "name": "generation_finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "generation_deadline_at": { + "name": "generation_deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "generation_revision": { + "name": "generation_revision", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "generation_max_cost": { + "name": "generation_max_cost", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "notification_sent_at": { + "name": "notification_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_briefing_reports_window": { + "name": "agent_briefing_reports_window", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "context_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "local_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_briefing_reports_user_history": { + "name": "agent_briefing_reports_user_history", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cutoff_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_briefing_reports_active": { + "name": "agent_briefing_reports_active", + "columns": [ + { + "expression": "morning_end_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_briefing_reports_refresh_due": { + "name": "agent_briefing_reports_refresh_due", + "columns": [ + { + "expression": "next_refresh_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_briefing_reports_charge_due": { + "name": "agent_briefing_reports_charge_due", + "columns": [ + { + "expression": "next_charge_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_briefing_reports_kilo_user_id_kilocode_users_id_fk": { + "name": "agent_briefing_reports_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "agent_briefing_reports", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_briefing_reports_organization_id_organizations_id_fk": { + "name": "agent_briefing_reports_organization_id_organizations_id_fk", + "tableFrom": "agent_briefing_reports", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_configs": { + "name": "agent_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_type": { + "name": "agent_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "runtime_state": { + "name": "runtime_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_by": { + "name": "created_by", + "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()" + }, + "config_revision": { + "name": "config_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "IDX_agent_configs_org_id": { + "name": "IDX_agent_configs_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_owned_by_user_id": { + "name": "IDX_agent_configs_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_agent_type": { + "name": "IDX_agent_configs_agent_type", + "columns": [ + { + "expression": "agent_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_configs_platform": { + "name": "IDX_agent_configs_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_configs_owned_by_organization_id_organizations_id_fk": { + "name": "agent_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_configs_org_agent_platform": { + "name": "UQ_agent_configs_org_agent_platform", + "nullsNotDistinct": false, + "columns": [ + "owned_by_organization_id", + "agent_type", + "platform" + ] + }, + "UQ_agent_configs_user_agent_platform": { + "name": "UQ_agent_configs_user_agent_platform", + "nullsNotDistinct": false, + "columns": [ + "owned_by_user_id", + "agent_type", + "platform" + ] + } + }, + "policies": {}, + "checkConstraints": { + "agent_configs_owner_check": { + "name": "agent_configs_owner_check", + "value": "(\n (\"agent_configs\".\"owned_by_user_id\" IS NOT NULL AND \"agent_configs\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_configs\".\"owned_by_user_id\" IS NULL AND \"agent_configs\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "agent_configs_agent_type_check": { + "name": "agent_configs_agent_type_check", + "value": "\"agent_configs\".\"agent_type\" IN ('code_review', 'auto_triage', 'auto_fix', 'security_scan')" + }, + "agent_configs_config_revision_check": { + "name": "agent_configs_config_revision_check", + "value": "\"agent_configs\".\"config_revision\" >= 1" + } + }, + "isRLSEnabled": false + }, + "public.agent_environment_profile_agents": { + "name": "agent_environment_profile_agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "IDX_agent_env_profile_agents_profile_id": { + "name": "IDX_agent_env_profile_agents_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_agents_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_agents_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_agents", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_agents_profile_slug": { + "name": "UQ_agent_env_profile_agents_profile_slug", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_commands": { + "name": "agent_environment_profile_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_commands_profile_id": { + "name": "IDX_agent_env_profile_commands_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_commands_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_commands_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_commands", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_commands_profile_sequence": { + "name": "UQ_agent_env_profile_commands_profile_sequence", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "sequence" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_instructions": { + "name": "agent_environment_profile_instructions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_instructions_profile_id": { + "name": "IDX_agent_env_profile_instructions_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_instructions_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_instructions_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_instructions", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_instructions_profile_name": { + "name": "UQ_agent_env_profile_instructions_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_kilo_commands": { + "name": "agent_environment_profile_kilo_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtask": { + "name": "subtask", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "IDX_agent_env_profile_kilo_cmds_profile_id": { + "name": "IDX_agent_env_profile_kilo_cmds_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_kilo_commands_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_kilo_commands_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_kilo_commands", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_kilo_cmds_profile_name": { + "name": "UQ_agent_env_profile_kilo_cmds_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_mcp_servers": { + "name": "agent_environment_profile_mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "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": { + "IDX_agent_env_profile_mcp_servers_profile_id": { + "name": "IDX_agent_env_profile_mcp_servers_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_mcp_servers_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_mcp_servers_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_mcp_servers", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_mcp_servers_profile_name": { + "name": "UQ_agent_env_profile_mcp_servers_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_repo_bindings": { + "name": "agent_environment_profile_repo_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_agent_env_profile_repo_bindings_user": { + "name": "UQ_agent_env_profile_repo_bindings_user", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profile_repo_bindings_org": { + "name": "UQ_agent_env_profile_repo_bindings_org", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_repo_bindings_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_repo_bindings_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profile_repo_bindings_owned_by_organization_id_organizations_id_fk": { + "name": "agent_environment_profile_repo_bindings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profile_repo_bindings_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_environment_profile_repo_bindings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_environment_profile_repo_bindings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_env_profile_repo_bindings_owner_check": { + "name": "agent_env_profile_repo_bindings_owner_check", + "value": "(\n (\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" IS NOT NULL AND \"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_environment_profile_repo_bindings\".\"owned_by_user_id\" IS NULL AND \"agent_environment_profile_repo_bindings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.agent_environment_profile_skills": { + "name": "agent_environment_profile_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_markdown": { + "name": "raw_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_agent_env_profile_skills_profile_id": { + "name": "IDX_agent_env_profile_skills_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_skills_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_skills_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_skills", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_skills_profile_name": { + "name": "UQ_agent_env_profile_skills_profile_name", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profile_vars": { + "name": "agent_environment_profile_vars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "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": { + "IDX_agent_env_profile_vars_profile_id": { + "name": "IDX_agent_env_profile_vars_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profile_vars_profile_id_agent_environment_profiles_id_fk": { + "name": "agent_environment_profile_vars_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "agent_environment_profile_vars", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_agent_env_profile_vars_profile_key": { + "name": "UQ_agent_env_profile_vars_profile_key", + "nullsNotDistinct": false, + "columns": [ + "profile_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_environment_profiles": { + "name": "agent_environment_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "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": { + "UQ_agent_env_profiles_org_name": { + "name": "UQ_agent_env_profiles_org_name", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_user_name": { + "name": "UQ_agent_env_profiles_user_name", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_org_default": { + "name": "UQ_agent_env_profiles_org_default", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"is_default\" = true AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_agent_env_profiles_user_default": { + "name": "UQ_agent_env_profiles_user_default", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"agent_environment_profiles\".\"is_default\" = true AND \"agent_environment_profiles\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_org_id": { + "name": "IDX_agent_env_profiles_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_user_id": { + "name": "IDX_agent_env_profiles_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_agent_env_profiles_created_by_user_id": { + "name": "IDX_agent_env_profiles_created_by_user_id", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_environment_profiles_owned_by_organization_id_organizations_id_fk": { + "name": "agent_environment_profiles_owned_by_organization_id_organizations_id_fk", + "tableFrom": "agent_environment_profiles", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_environment_profiles_owned_by_user_id_kilocode_users_id_fk": { + "name": "agent_environment_profiles_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "agent_environment_profiles", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "agent_env_profiles_owner_check": { + "name": "agent_env_profiles_owner_check", + "value": "(\n (\"agent_environment_profiles\".\"owned_by_user_id\" IS NOT NULL AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NULL) OR\n (\"agent_environment_profiles\".\"owned_by_user_id\" IS NULL AND \"agent_environment_profiles\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.ai_gateway_config": { + "name": "ai_gateway_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "default": 1 + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "auto_free": { + "name": "auto_free", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "preferred_models": { + "name": "preferred_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "unavailable_models": { + "name": "unavailable_models", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "vercel_model_id_overrides": { + "name": "vercel_model_id_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "preferred_provider_order": { + "name": "preferred_provider_order", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ai_gateway_config_singleton": { + "name": "ai_gateway_config_singleton", + "value": "\"ai_gateway_config\".\"id\" = 1" + } + }, + "isRLSEnabled": false + }, + "public.ai_gateway_request_logging_opt_ins": { + "name": "ai_gateway_request_logging_opt_ins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "default": 1 + }, + "opt_ins": { + "name": "opt_ins", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ai_gateway_request_logging_opt_ins_singleton": { + "name": "ai_gateway_request_logging_opt_ins_singleton", + "value": "\"ai_gateway_request_logging_opt_ins\".\"id\" = 1" + } + }, + "isRLSEnabled": false + }, + "public.ai_gateway_sync_providers_state": { + "name": "ai_gateway_sync_providers_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "default": 1 + }, + "last_completed_at": { + "name": "last_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stale_alert_last_posted_at": { + "name": "stale_alert_last_posted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "direct_byok_last_completed_at": { + "name": "direct_byok_last_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "direct_byok_stale_alert_last_posted_at": { + "name": "direct_byok_stale_alert_last_posted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ai_gateway_sync_providers_state_singleton": { + "name": "ai_gateway_sync_providers_state_singleton", + "value": "\"ai_gateway_sync_providers_state\".\"id\" = 1" + } + }, + "isRLSEnabled": false + }, + "public.analytics_event_outbox": { + "name": "analytics_event_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "event_uuid": { + "name": "event_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_name": { + "name": "event_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "distinct_id": { + "name": "distinct_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_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()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_analytics_event_outbox_event_uuid": { + "name": "UQ_analytics_event_outbox_event_uuid", + "columns": [ + { + "expression": "event_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_analytics_event_outbox_status_next_attempt_at": { + "name": "IDX_analytics_event_outbox_status_next_attempt_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_kind": { + "name": "api_kind", + "schema": "", + "columns": { + "api_kind_id": { + "name": "api_kind_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "api_kind": { + "name": "api_kind", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_api_kind": { + "name": "UQ_api_kind", + "columns": [ + { + "expression": "api_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_request_log": { + "name": "api_request_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_request_id": { + "name": "vercel_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response": { + "name": "response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "request_r2_key": { + "name": "request_r2_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_r2_key": { + "name": "response_r2_key", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_api_request_log_created_at": { + "name": "idx_api_request_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_min_versions": { + "name": "app_min_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "ios_min_version": { + "name": "ios_min_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "android_min_version": { + "name": "android_min_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.app_reported_messages": { + "name": "app_reported_messages", + "schema": "", + "columns": { + "report_id": { + "name": "report_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "report_type": { + "name": "report_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature": { + "name": "signature", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "app_reported_messages_cli_session_id_cli_sessions_session_id_fk": { + "name": "app_reported_messages_cli_session_id_cli_sessions_session_id_fk", + "tableFrom": "app_reported_messages", + "tableTo": "cli_sessions", + "columnsFrom": [ + "cli_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_model": { + "name": "auto_model", + "schema": "", + "columns": { + "auto_model_id": { + "name": "auto_model_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auto_model": { + "name": "auto_model", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_auto_model": { + "name": "UQ_auto_model", + "columns": [ + { + "expression": "auto_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_routing_custom_routers": { + "name": "auto_routing_custom_routers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_router": { + "name": "base_router", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "default_entry_key": { + "name": "default_entry_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "legacy_pool": { + "name": "legacy_pool", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "legacy_mode_inherited": { + "name": "legacy_mode_inherited", + "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": { + "UQ_auto_routing_custom_routers_org_slug": { + "name": "UQ_auto_routing_custom_routers_org_slug", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_routing_custom_routers\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_auto_routing_custom_routers_user_slug": { + "name": "UQ_auto_routing_custom_routers_user_slug", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_routing_custom_routers\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_routing_custom_routers_org_id": { + "name": "IDX_auto_routing_custom_routers_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_auto_routing_custom_routers_user_id": { + "name": "IDX_auto_routing_custom_routers_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_routing_custom_routers_owned_by_organization_id_organizations_id_fk": { + "name": "auto_routing_custom_routers_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_routing_custom_routers", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_routing_custom_routers_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_routing_custom_routers_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_routing_custom_routers", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_routing_custom_routers_owner_check": { + "name": "auto_routing_custom_routers_owner_check", + "value": "(\n (\"auto_routing_custom_routers\".\"owned_by_user_id\" IS NOT NULL AND \"auto_routing_custom_routers\".\"owned_by_organization_id\" IS NULL) OR\n (\"auto_routing_custom_routers\".\"owned_by_user_id\" IS NULL AND \"auto_routing_custom_routers\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "auto_routing_custom_routers_base_router_check": { + "name": "auto_routing_custom_routers_base_router_check", + "value": "\"auto_routing_custom_routers\".\"base_router\" IN ('efficient', 'frontier')" + }, + "auto_routing_custom_routers_mode_check": { + "name": "auto_routing_custom_routers_mode_check", + "value": "\"auto_routing_custom_routers\".\"mode\" IN ('cost_per_accuracy', 'best_accuracy')" + } + }, + "isRLSEnabled": false + }, + "public.auto_top_up_configs": { + "name": "auto_top_up_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_method_id": { + "name": "stripe_payment_method_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5000 + }, + "threshold_cents": { + "name": "threshold_cents", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_auto_top_up_at": { + "name": "last_auto_top_up_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_started_at": { + "name": "attempt_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "disabled_reason": { + "name": "disabled_reason", + "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": { + "UQ_auto_top_up_configs_owned_by_user_id": { + "name": "UQ_auto_top_up_configs_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_top_up_configs\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_auto_top_up_configs_owned_by_organization_id": { + "name": "UQ_auto_top_up_configs_owned_by_organization_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"auto_top_up_configs\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_top_up_configs_owned_by_user_id_kilocode_users_id_fk": { + "name": "auto_top_up_configs_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "auto_top_up_configs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "auto_top_up_configs_owned_by_organization_id_organizations_id_fk": { + "name": "auto_top_up_configs_owned_by_organization_id_organizations_id_fk", + "tableFrom": "auto_top_up_configs", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "auto_top_up_configs_exactly_one_owner": { + "name": "auto_top_up_configs_exactly_one_owner", + "value": "(\"auto_top_up_configs\".\"owned_by_user_id\" IS NOT NULL AND \"auto_top_up_configs\".\"owned_by_organization_id\" IS NULL) OR (\"auto_top_up_configs\".\"owned_by_user_id\" IS NULL AND \"auto_top_up_configs\".\"owned_by_organization_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.bot_request_cloud_agent_sessions": { + "name": "bot_request_cloud_agent_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "bot_request_id": { + "name": "bot_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "spawn_group_id": { + "name": "spawn_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlab_project": { + "name": "gitlab_project", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "callback_step": { + "name": "callback_step", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_message": { + "name": "final_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_message_fetched_at": { + "name": "final_message_fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "final_message_error": { + "name": "final_message_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "continuation_started_at": { + "name": "continuation_started_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": { + "UQ_bot_request_cas_cloud_agent_session_id": { + "name": "UQ_bot_request_cas_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id": { + "name": "IDX_bot_request_cas_bot_request_id", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id_spawn_group_id": { + "name": "IDX_bot_request_cas_bot_request_id_spawn_group_id", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spawn_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_request_cas_bot_request_id_spawn_group_id_status": { + "name": "IDX_bot_request_cas_bot_request_id_spawn_group_id_status", + "columns": [ + { + "expression": "bot_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spawn_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_request_cloud_agent_sessions_bot_request_id_bot_requests_id_fk": { + "name": "bot_request_cloud_agent_sessions_bot_request_id_bot_requests_id_fk", + "tableFrom": "bot_request_cloud_agent_sessions", + "tableTo": "bot_requests", + "columnsFrom": [ + "bot_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bot_requests": { + "name": "bot_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_thread_id": { + "name": "platform_thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_message_id": { + "name": "platform_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "steps": { + "name": "steps", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "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": { + "IDX_bot_requests_created_at": { + "name": "IDX_bot_requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_created_by": { + "name": "IDX_bot_requests_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_organization_id": { + "name": "IDX_bot_requests_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_platform_integration_id": { + "name": "IDX_bot_requests_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bot_requests_status": { + "name": "IDX_bot_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bot_requests_created_by_kilocode_users_id_fk": { + "name": "bot_requests_created_by_kilocode_users_id_fk", + "tableFrom": "bot_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_requests_organization_id_organizations_id_fk": { + "name": "bot_requests_organization_id_organizations_id_fk", + "tableFrom": "bot_requests", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bot_requests_platform_integration_id_platform_integrations_id_fk": { + "name": "bot_requests_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "bot_requests", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bouncer_credit_event_outbox": { + "name": "bouncer_credit_event_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_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()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_bouncer_credit_event_outbox_event_id_type": { + "name": "UQ_bouncer_credit_event_outbox_event_id_type", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bouncer_credit_event_outbox_status_next_attempt_at": { + "name": "IDX_bouncer_credit_event_outbox_status_next_attempt_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bouncer_credit_event_outbox_user_id": { + "name": "IDX_bouncer_credit_event_outbox_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bouncer_usage_event_outbox": { + "name": "bouncer_usage_event_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_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()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_bouncer_usage_event_outbox_request_id": { + "name": "UQ_bouncer_usage_event_outbox_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bouncer_usage_event_outbox_status_next_attempt_at": { + "name": "IDX_bouncer_usage_event_outbox_status_next_attempt_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_bouncer_usage_event_outbox_user_id": { + "name": "IDX_bouncer_usage_event_outbox_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.byok_api_keys": { + "name": "byok_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "management_source": { + "name": "management_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_byok_api_keys_organization_id": { + "name": "IDX_byok_api_keys_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_byok_api_keys_kilo_user_id": { + "name": "IDX_byok_api_keys_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_byok_api_keys_provider_id": { + "name": "IDX_byok_api_keys_provider_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "byok_api_keys_organization_id_organizations_id_fk": { + "name": "byok_api_keys_organization_id_organizations_id_fk", + "tableFrom": "byok_api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "byok_api_keys_kilo_user_id_kilocode_users_id_fk": { + "name": "byok_api_keys_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "byok_api_keys", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_byok_api_keys_org_provider": { + "name": "UQ_byok_api_keys_org_provider", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "provider_id" + ] + }, + "UQ_byok_api_keys_user_provider": { + "name": "UQ_byok_api_keys_user_provider", + "nullsNotDistinct": false, + "columns": [ + "kilo_user_id", + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "byok_api_keys_management_source_check": { + "name": "byok_api_keys_management_source_check", + "value": "\"byok_api_keys\".\"management_source\" IN ('user', 'coding_plan')" + }, + "byok_api_keys_owner_check": { + "name": "byok_api_keys_owner_check", + "value": "(\n (\"byok_api_keys\".\"kilo_user_id\" IS NOT NULL AND \"byok_api_keys\".\"organization_id\" IS NULL) OR\n (\"byok_api_keys\".\"kilo_user_id\" IS NULL AND \"byok_api_keys\".\"organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cli_sessions": { + "name": "cli_sessions", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_on_platform": { + "name": "created_on_platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "api_conversation_history_blob_url": { + "name": "api_conversation_history_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_metadata_blob_url": { + "name": "task_metadata_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ui_messages_blob_url": { + "name": "ui_messages_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_state_blob_url": { + "name": "git_state_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forked_from": { + "name": "forked_from", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_session_id": { + "name": "parent_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_mode": { + "name": "last_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_model": { + "name": "last_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "IDX_cli_sessions_kilo_user_id": { + "name": "IDX_cli_sessions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_created_at": { + "name": "IDX_cli_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_updated_at": { + "name": "IDX_cli_sessions_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_organization_id": { + "name": "IDX_cli_sessions_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_user_updated": { + "name": "IDX_cli_sessions_user_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "cli_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cli_sessions_forked_from_cli_sessions_session_id_fk": { + "name": "cli_sessions_forked_from_cli_sessions_session_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "forked_from" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_parent_session_id_cli_sessions_session_id_fk": { + "name": "cli_sessions_parent_session_id_cli_sessions_session_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "parent_session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_organization_id_organizations_id_fk": { + "name": "cli_sessions_organization_id_organizations_id_fk", + "tableFrom": "cli_sessions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cli_sessions_cloud_agent_session_id_unique": { + "name": "cli_sessions_cloud_agent_session_id_unique", + "nullsNotDistinct": false, + "columns": [ + "cloud_agent_session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_sessions_v2": { + "name": "cli_sessions_v2", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_session_id": { + "name": "parent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_scope_id": { + "name": "cloud_agent_session_scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_worktree_id": { + "name": "cloud_agent_worktree_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_on_platform": { + "name": "created_on_platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_head_ref": { + "name": "pr_head_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_head_sha": { + "name": "pr_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_link_verified_at": { + "name": "pr_link_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_updated_at": { + "name": "status_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status_transition_id": { + "name": "status_transition_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "live_summary": { + "name": "live_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cli_sessions_v2_parent_session_id_kilo_user_id": { + "name": "IDX_cli_sessions_v2_parent_session_id_kilo_user_id", + "columns": [ + { + "expression": "parent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_profile_id": { + "name": "IDX_cli_sessions_v2_profile_id", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_cli_sessions_v2_public_id": { + "name": "UQ_cli_sessions_v2_public_id", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cli_sessions_v2\".\"public_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cli_sessions_v2_cloud_agent_session_id": { + "name": "UQ_cli_sessions_v2_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cli_sessions_v2\".\"cloud_agent_session_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_organization_id": { + "name": "IDX_cli_sessions_v2_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_user_updated": { + "name": "IDX_cli_sessions_v2_user_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_user_created": { + "name": "IDX_cli_sessions_v2_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_user_worktree_updated": { + "name": "IDX_cli_sessions_v2_user_worktree_updated", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cloud_agent_worktree_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cli_sessions_v2\".\"cloud_agent_worktree_id\" is not null", + "concurrently": true, + "method": "btree", + "with": {} + }, + "cli_sessions_v2_git_url_branch_idx": { + "name": "cli_sessions_v2_git_url_branch_idx", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cli_sessions_v2_git_url_pr_number": { + "name": "IDX_cli_sessions_v2_git_url_pr_number", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_sessions_v2_kilo_user_id_kilocode_users_id_fk": { + "name": "cli_sessions_v2_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cli_sessions_v2_organization_id_organizations_id_fk": { + "name": "cli_sessions_v2_organization_id_organizations_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_v2_profile_id_agent_environment_profiles_id_fk": { + "name": "cli_sessions_v2_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_sessions_v2_parent_session_id_kilo_user_id_fk": { + "name": "cli_sessions_v2_parent_session_id_kilo_user_id_fk", + "tableFrom": "cli_sessions_v2", + "tableTo": "cli_sessions_v2", + "columnsFrom": [ + "parent_session_id", + "kilo_user_id" + ], + "columnsTo": [ + "session_id", + "kilo_user_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "cli_sessions_v2_session_id_kilo_user_id_pk": { + "name": "cli_sessions_v2_session_id_kilo_user_id_pk", + "columns": [ + "session_id", + "kilo_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_agent_code_review_attempts": { + "name": "cloud_agent_code_review_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_review_id": { + "name": "code_review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "retry_of_attempt_id": { + "name": "retry_of_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "retry_reason": { + "name": "retry_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analytics_enabled_at_dispatch": { + "name": "analytics_enabled_at_dispatch", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_reason": { + "name": "terminal_reason", + "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 + }, + "publication_status": { + "name": "publication_status", + "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": { + "UQ_cloud_agent_code_review_attempts_review_attempt_number": { + "name": "UQ_cloud_agent_code_review_attempts_review_attempt_number", + "columns": [ + { + "expression": "code_review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_code_review_id": { + "name": "idx_cloud_agent_code_review_attempts_code_review_id", + "columns": [ + { + "expression": "code_review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_retry_of_attempt_id": { + "name": "idx_cloud_agent_code_review_attempts_retry_of_attempt_id", + "columns": [ + { + "expression": "retry_of_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_session_id": { + "name": "idx_cloud_agent_code_review_attempts_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_cli_session_id": { + "name": "idx_cloud_agent_code_review_attempts_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_status": { + "name": "idx_cloud_agent_code_review_attempts_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_review_attempts_retry_reason": { + "name": "idx_cloud_agent_code_review_attempts_retry_reason", + "columns": [ + { + "expression": "retry_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_code_review_attempts_code_review_id_cloud_agent_code_reviews_id_fk": { + "name": "cloud_agent_code_review_attempts_code_review_id_cloud_agent_code_reviews_id_fk", + "tableFrom": "cloud_agent_code_review_attempts", + "tableTo": "cloud_agent_code_reviews", + "columnsFrom": [ + "code_review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_review_attempts_retry_of_attempt_id_cloud_agent_code_review_attempts_id_fk": { + "name": "cloud_agent_code_review_attempts_retry_of_attempt_id_cloud_agent_code_review_attempts_id_fk", + "tableFrom": "cloud_agent_code_review_attempts", + "tableTo": "cloud_agent_code_review_attempts", + "columnsFrom": [ + "retry_of_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_code_review_attempts_attempt_number_check": { + "name": "cloud_agent_code_review_attempts_attempt_number_check", + "value": "\"cloud_agent_code_review_attempts\".\"attempt_number\" >= 1" + }, + "cloud_agent_code_review_attempts_publication_status_check": { + "name": "cloud_agent_code_review_attempts_publication_status_check", + "value": "\"cloud_agent_code_review_attempts\".\"publication_status\" IN ('published', 'unchanged', 'missing', 'unknown', 'not_applicable')" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_code_reviews": { + "name": "cloud_agent_code_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "manual_config": { + "name": "manual_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "review_type": { + "name": "review_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "council_result": { + "name": "council_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_title": { + "name": "pr_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_author": { + "name": "pr_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_author_github_id": { + "name": "pr_author_github_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_ref": { + "name": "head_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "platform_project_id": { + "name": "platform_project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "dispatch_reservation_id": { + "name": "dispatch_reservation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terminal_reason": { + "name": "terminal_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_version": { + "name": "agent_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'v1'" + }, + "check_run_id": { + "name": "check_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "repository_review_instructions_used": { + "name": "repository_review_instructions_used", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "repository_review_instructions_ref": { + "name": "repository_review_instructions_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_review_instructions_truncated": { + "name": "repository_review_instructions_truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previous_summary_body": { + "name": "previous_summary_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_summary_head_sha": { + "name": "previous_summary_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_summary_observed": { + "name": "previous_summary_observed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_tokens_in": { + "name": "total_tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_tokens_out": { + "name": "total_tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_cost_musd": { + "name": "total_cost_musd", + "type": "integer", + "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": { + "UQ_cloud_agent_code_reviews_webhook_integration_repo_pr_sha": { + "name": "UQ_cloud_agent_code_reviews_webhook_integration_repo_pr_sha", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "head_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_code_reviews\".\"manual_config\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_code_reviews_active_provider_publisher": { + "name": "UQ_cloud_agent_code_reviews_active_provider_publisher", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_code_reviews\".\"platform_integration_id\" IS NOT NULL\n AND \"cloud_agent_code_reviews\".\"status\" IN ('pending', 'queued', 'running')\n AND (\"cloud_agent_code_reviews\".\"manual_config\" IS NULL OR \"cloud_agent_code_reviews\".\"manual_config\"->>'outputMode' = 'provider')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_owned_by_org_id": { + "name": "idx_cloud_agent_code_reviews_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_owned_by_user_id": { + "name": "idx_cloud_agent_code_reviews_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_session_id": { + "name": "idx_cloud_agent_code_reviews_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_cli_session_id": { + "name": "idx_cloud_agent_code_reviews_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_status": { + "name": "idx_cloud_agent_code_reviews_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_repo": { + "name": "idx_cloud_agent_code_reviews_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_pr_number": { + "name": "idx_cloud_agent_code_reviews_pr_number", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_created_at": { + "name": "idx_cloud_agent_code_reviews_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_pr_author_github_id": { + "name": "idx_cloud_agent_code_reviews_pr_author_github_id", + "columns": [ + { + "expression": "pr_author_github_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_completed_at": { + "name": "idx_cloud_agent_code_reviews_completed_at", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_code_reviews\".\"completed_at\" is not null", + "concurrently": true, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_started_at": { + "name": "idx_cloud_agent_code_reviews_started_at", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_code_reviews\".\"started_at\" is not null", + "concurrently": true, + "method": "btree", + "with": {} + }, + "idx_cloud_agent_code_reviews_terminal_missing_completed_at": { + "name": "idx_cloud_agent_code_reviews_terminal_missing_completed_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_code_reviews\".\"status\" IN ('completed', 'failed', 'cancelled', 'interrupted') AND \"cloud_agent_code_reviews\".\"completed_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_code_reviews_owned_by_organization_id_organizations_id_fk": { + "name": "cloud_agent_code_reviews_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_reviews_owned_by_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_code_reviews_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_code_reviews_platform_integration_id_platform_integrations_id_fk": { + "name": "cloud_agent_code_reviews_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "cloud_agent_code_reviews", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_code_reviews_owner_check": { + "name": "cloud_agent_code_reviews_owner_check", + "value": "(\n (\"cloud_agent_code_reviews\".\"owned_by_user_id\" IS NOT NULL AND \"cloud_agent_code_reviews\".\"owned_by_organization_id\" IS NULL) OR\n (\"cloud_agent_code_reviews\".\"owned_by_user_id\" IS NULL AND \"cloud_agent_code_reviews\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_environment_vars": { + "name": "cloud_agent_environment_vars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "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": { + "IDX_cloud_agent_environment_vars_environment_id": { + "name": "IDX_cloud_agent_environment_vars_environment_id", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_environment_vars_environment_id_cloud_agent_environments_id_fk": { + "name": "cloud_agent_environment_vars_environment_id_cloud_agent_environments_id_fk", + "tableFrom": "cloud_agent_environment_vars", + "tableTo": "cloud_agent_environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_cloud_agent_environment_vars_environment_key": { + "name": "UQ_cloud_agent_environment_vars_environment_key", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_agent_environments": { + "name": "cloud_agent_environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gitlab_clone_url": { + "name": "gitlab_clone_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucket_workspace_uuid": { + "name": "bitbucket_workspace_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "bitbucket_repository_uuid": { + "name": "bitbucket_repository_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "commands": { + "name": "commands", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "sandbox_allocation": { + "name": "sandbox_allocation", + "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": { + "UQ_cloud_agent_environments_github_user": { + "name": "UQ_cloud_agent_environments_github_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_environments\".\"platform\" = 'github' AND \"cloud_agent_environments\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_environments_github_org": { + "name": "UQ_cloud_agent_environments_github_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_environments\".\"platform\" = 'github' AND \"cloud_agent_environments\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_environments_gitlab_user": { + "name": "UQ_cloud_agent_environments_gitlab_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gitlab_clone_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_environments\".\"platform\" = 'gitlab' AND \"cloud_agent_environments\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_environments_gitlab_org": { + "name": "UQ_cloud_agent_environments_gitlab_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "gitlab_clone_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_environments\".\"platform\" = 'gitlab' AND \"cloud_agent_environments\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_environments_bitbucket_user": { + "name": "UQ_cloud_agent_environments_bitbucket_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bitbucket_workspace_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bitbucket_repository_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_environments\".\"platform\" = 'bitbucket' AND \"cloud_agent_environments\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_environments_bitbucket_org": { + "name": "UQ_cloud_agent_environments_bitbucket_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bitbucket_workspace_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bitbucket_repository_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_environments\".\"platform\" = 'bitbucket' AND \"cloud_agent_environments\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_environments_user_id": { + "name": "IDX_cloud_agent_environments_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_environments_org_id": { + "name": "IDX_cloud_agent_environments_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_environments_owned_by_organization_id_organizations_id_fk": { + "name": "cloud_agent_environments_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_environments", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_environments_owned_by_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_environments_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_environments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_environments_owner_check": { + "name": "cloud_agent_environments_owner_check", + "value": "(\n (\"cloud_agent_environments\".\"owned_by_user_id\" IS NOT NULL AND \"cloud_agent_environments\".\"owned_by_organization_id\" IS NULL) OR\n (\"cloud_agent_environments\".\"owned_by_user_id\" IS NULL AND \"cloud_agent_environments\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "cloud_agent_environments_platform_check": { + "name": "cloud_agent_environments_platform_check", + "value": "\"cloud_agent_environments\".\"platform\" IN ('github', 'gitlab', 'bitbucket')" + }, + "cloud_agent_environments_github_identity_check": { + "name": "cloud_agent_environments_github_identity_check", + "value": "(\n (\"cloud_agent_environments\".\"platform\" = 'github') = (\n \"cloud_agent_environments\".\"gitlab_clone_url\" IS NULL AND\n \"cloud_agent_environments\".\"bitbucket_workspace_uuid\" IS NULL AND\n \"cloud_agent_environments\".\"bitbucket_repository_uuid\" IS NULL\n )\n )" + }, + "cloud_agent_environments_gitlab_identity_check": { + "name": "cloud_agent_environments_gitlab_identity_check", + "value": "(\n (\"cloud_agent_environments\".\"platform\" = 'gitlab') = (\n \"cloud_agent_environments\".\"gitlab_clone_url\" IS NOT NULL AND\n \"cloud_agent_environments\".\"bitbucket_workspace_uuid\" IS NULL AND\n \"cloud_agent_environments\".\"bitbucket_repository_uuid\" IS NULL\n )\n )" + }, + "cloud_agent_environments_bitbucket_identity_check": { + "name": "cloud_agent_environments_bitbucket_identity_check", + "value": "(\n (\"cloud_agent_environments\".\"platform\" = 'bitbucket') = (\n \"cloud_agent_environments\".\"bitbucket_workspace_uuid\" IS NOT NULL AND\n \"cloud_agent_environments\".\"bitbucket_repository_uuid\" IS NOT NULL AND\n \"cloud_agent_environments\".\"gitlab_clone_url\" IS NULL\n )\n )" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_feedback": { + "name": "cloud_agent_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_type": { + "name": "session_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_streaming": { + "name": "is_streaming", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recent_messages": { + "name": "recent_messages", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cloud_agent_feedback_created_at": { + "name": "IDX_cloud_agent_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_feedback_kilo_user_id": { + "name": "IDX_cloud_agent_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_feedback_cloud_agent_session_id": { + "name": "IDX_cloud_agent_feedback_cloud_agent_session_id", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "cloud_agent_feedback_organization_id_organizations_id_fk": { + "name": "cloud_agent_feedback_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_feedback", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloud_agent_pending_uploads": { + "name": "cloud_agent_pending_uploads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_uuid": { + "name": "message_uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attachment_id": { + "name": "attachment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "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": { + "IDX_cloud_agent_pending_uploads_user_message_status": { + "name": "IDX_cloud_agent_pending_uploads_user_message_status", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_pending_uploads_expired": { + "name": "IDX_cloud_agent_pending_uploads_expired", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_pending_uploads\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cloud_agent_pending_uploads_object_key_unique": { + "name": "cloud_agent_pending_uploads_object_key_unique", + "nullsNotDistinct": false, + "columns": [ + "object_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "cloud_agent_pending_uploads_status_check": { + "name": "cloud_agent_pending_uploads_status_check", + "value": "\"cloud_agent_pending_uploads\".\"status\" IN ('pending', 'linked', 'reaped')" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_sandbox_allocation_settings": { + "name": "cloud_agent_sandbox_allocation_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_full_name": { + "name": "repository_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_platform": { + "name": "repository_platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allocation": { + "name": "allocation", + "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": { + "UQ_cloud_agent_sandbox_alloc_org_context": { + "name": "UQ_cloud_agent_sandbox_alloc_org_context", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_sandbox_allocation_settings\".\"owned_by_organization_id\" IS NOT NULL AND \"cloud_agent_sandbox_allocation_settings\".\"repository_full_name\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_sandbox_alloc_user_context": { + "name": "UQ_cloud_agent_sandbox_alloc_user_context", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_sandbox_allocation_settings\".\"owned_by_user_id\" IS NOT NULL AND \"cloud_agent_sandbox_allocation_settings\".\"repository_full_name\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_sandbox_alloc_org_repo": { + "name": "UQ_cloud_agent_sandbox_alloc_org_repo", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repository_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repository_platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_sandbox_allocation_settings\".\"owned_by_organization_id\" IS NOT NULL AND \"cloud_agent_sandbox_allocation_settings\".\"repository_full_name\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_sandbox_alloc_user_repo": { + "name": "UQ_cloud_agent_sandbox_alloc_user_repo", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repository_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repository_platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_sandbox_allocation_settings\".\"owned_by_user_id\" IS NOT NULL AND \"cloud_agent_sandbox_allocation_settings\".\"repository_full_name\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sandbox_alloc_org": { + "name": "IDX_cloud_agent_sandbox_alloc_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sandbox_alloc_user": { + "name": "IDX_cloud_agent_sandbox_alloc_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_sandbox_allocation_settings_owned_by_organization_id_organizations_id_fk": { + "name": "cloud_agent_sandbox_allocation_settings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_sandbox_allocation_settings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_sandbox_allocation_settings_owned_by_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_sandbox_allocation_settings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_sandbox_allocation_settings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_sandbox_allocation_settings_owner_check": { + "name": "cloud_agent_sandbox_allocation_settings_owner_check", + "value": "(\n (\"cloud_agent_sandbox_allocation_settings\".\"owned_by_user_id\" IS NOT NULL AND \"cloud_agent_sandbox_allocation_settings\".\"owned_by_organization_id\" IS NULL) OR\n (\"cloud_agent_sandbox_allocation_settings\".\"owned_by_user_id\" IS NULL AND \"cloud_agent_sandbox_allocation_settings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "cloud_agent_sandbox_allocation_settings_repo_pair_check": { + "name": "cloud_agent_sandbox_allocation_settings_repo_pair_check", + "value": "(\n (\"cloud_agent_sandbox_allocation_settings\".\"repository_full_name\" IS NULL AND \"cloud_agent_sandbox_allocation_settings\".\"repository_platform\" IS NULL) OR\n (\"cloud_agent_sandbox_allocation_settings\".\"repository_full_name\" IS NOT NULL AND \"cloud_agent_sandbox_allocation_settings\".\"repository_platform\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_session_runs": { + "name": "cloud_agent_session_runs", + "schema": "", + "columns": { + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wrapper_run_id": { + "name": "wrapper_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "container": { + "name": "container", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dispatch_accepted_at": { + "name": "dispatch_accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "agent_activity_observed_at": { + "name": "agent_activity_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_stage": { + "name": "failure_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_responsibility": { + "name": "failure_responsibility", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message_redacted": { + "name": "error_message_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_expires_at": { + "name": "error_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_cloud_agent_session_runs_wrapper_run_id": { + "name": "IDX_cloud_agent_session_runs_wrapper_run_id", + "columns": [ + { + "expression": "wrapper_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"wrapper_run_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_session_queued": { + "name": "IDX_cloud_agent_session_runs_session_queued", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_queued_at": { + "name": "IDX_cloud_agent_session_runs_queued_at", + "columns": [ + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_terminal_at": { + "name": "IDX_cloud_agent_session_runs_terminal_at", + "columns": [ + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_status_terminal": { + "name": "IDX_cloud_agent_session_runs_status_terminal", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_failure_terminal": { + "name": "IDX_cloud_agent_session_runs_failure_terminal", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_responsibility_reason_terminal": { + "name": "IDX_cloud_agent_session_runs_responsibility_reason_terminal", + "columns": [ + { + "expression": "failure_responsibility", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terminal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"status\" = 'failed'", + "concurrently": true, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_session_runs_error_expires_at": { + "name": "IDX_cloud_agent_session_runs_error_expires_at", + "columns": [ + { + "expression": "error_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_session_runs\".\"error_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_session_runs_cloud_agent_session_id_cloud_agent_sessions_cloud_agent_session_id_fk": { + "name": "cloud_agent_session_runs_cloud_agent_session_id_cloud_agent_sessions_cloud_agent_session_id_fk", + "tableFrom": "cloud_agent_session_runs", + "tableTo": "cloud_agent_sessions", + "columnsFrom": [ + "cloud_agent_session_id" + ], + "columnsTo": [ + "cloud_agent_session_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "cloud_agent_session_runs_cloud_agent_session_id_message_id_pk": { + "name": "cloud_agent_session_runs_cloud_agent_session_id_message_id_pk", + "columns": [ + "cloud_agent_session_id", + "message_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_session_runs_status_check": { + "name": "cloud_agent_session_runs_status_check", + "value": "\"cloud_agent_session_runs\".\"status\" IN ('queued', 'accepted', 'completed', 'failed', 'interrupted')" + }, + "cloud_agent_session_runs_error_message_bounded_check": { + "name": "cloud_agent_session_runs_error_message_bounded_check", + "value": "\"cloud_agent_session_runs\".\"error_message_redacted\" IS NULL OR char_length(\"cloud_agent_session_runs\".\"error_message_redacted\") <= 4096" + }, + "cloud_agent_session_runs_error_expiry_check": { + "name": "cloud_agent_session_runs_error_expiry_check", + "value": "(\"cloud_agent_session_runs\".\"error_message_redacted\" IS NULL AND \"cloud_agent_session_runs\".\"error_expires_at\" IS NULL) OR\n (\"cloud_agent_session_runs\".\"error_message_redacted\" IS NOT NULL AND \"cloud_agent_session_runs\".\"error_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_sessions": { + "name": "cloud_agent_sessions", + "schema": "", + "columns": { + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initial_message_id": { + "name": "initial_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "failure_at": { + "name": "failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_stage": { + "name": "failure_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_responsibility": { + "name": "failure_responsibility", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message_redacted": { + "name": "error_message_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_expires_at": { + "name": "error_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_cloud_agent_sessions_kilo_session_id": { + "name": "UQ_cloud_agent_sessions_kilo_session_id", + "columns": [ + { + "expression": "kilo_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_sessions_initial_message_id": { + "name": "UQ_cloud_agent_sessions_initial_message_id", + "columns": [ + { + "expression": "initial_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_sandbox_id": { + "name": "IDX_cloud_agent_sessions_sandbox_id", + "columns": [ + { + "expression": "sandbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"sandbox_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_created_at": { + "name": "IDX_cloud_agent_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_created": { + "name": "IDX_cloud_agent_sessions_failure_created", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_at": { + "name": "IDX_cloud_agent_sessions_failure_at", + "columns": [ + { + "expression": "failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"failure_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_failure_classification_at": { + "name": "IDX_cloud_agent_sessions_failure_classification_at", + "columns": [ + { + "expression": "failure_stage", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"failure_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_sessions_error_expires_at": { + "name": "IDX_cloud_agent_sessions_error_expires_at", + "columns": [ + { + "expression": "error_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_sessions\".\"error_expires_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_sessions_failure_classification_check": { + "name": "cloud_agent_sessions_failure_classification_check", + "value": "(\"cloud_agent_sessions\".\"failure_at\" IS NULL AND \"cloud_agent_sessions\".\"failure_stage\" IS NULL AND \"cloud_agent_sessions\".\"failure_code\" IS NULL) OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'sandbox_identity' AND \"cloud_agent_sessions\".\"failure_code\" = 'sandbox_id_derivation_failed') OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'registration' AND \"cloud_agent_sessions\".\"failure_code\" = 'do_registration_rejected') OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'initial_admission' AND \"cloud_agent_sessions\".\"failure_code\" IN ('initial_admission_rejected', 'initial_queue_full', 'invalid_initial_intent')) OR\n (\"cloud_agent_sessions\".\"failure_at\" IS NOT NULL AND \"cloud_agent_sessions\".\"failure_stage\" = 'transport' AND \"cloud_agent_sessions\".\"failure_code\" = 'do_rpc_outcome_unknown')" + }, + "cloud_agent_sessions_error_message_bounded_check": { + "name": "cloud_agent_sessions_error_message_bounded_check", + "value": "\"cloud_agent_sessions\".\"error_message_redacted\" IS NULL OR char_length(\"cloud_agent_sessions\".\"error_message_redacted\") <= 4096" + }, + "cloud_agent_sessions_error_expiry_check": { + "name": "cloud_agent_sessions_error_expiry_check", + "value": "(\"cloud_agent_sessions\".\"error_message_redacted\" IS NULL AND \"cloud_agent_sessions\".\"error_expires_at\" IS NULL) OR\n (\"cloud_agent_sessions\".\"error_message_redacted\" IS NOT NULL AND \"cloud_agent_sessions\".\"error_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_webhook_triggers": { + "name": "cloud_agent_webhook_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger_id": { + "name": "trigger_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'cloud_agent'" + }, + "kiloclaw_instance_id": { + "name": "kiloclaw_instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "activation_mode": { + "name": "activation_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'webhook'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_timezone": { + "name": "cron_timezone", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'UTC'" + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "profile_id": { + "name": "profile_id", + "type": "uuid", + "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": { + "UQ_cloud_agent_webhook_triggers_user_trigger": { + "name": "UQ_cloud_agent_webhook_triggers_user_trigger", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_webhook_triggers\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_cloud_agent_webhook_triggers_org_trigger": { + "name": "UQ_cloud_agent_webhook_triggers_org_trigger", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"cloud_agent_webhook_triggers\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_user": { + "name": "IDX_cloud_agent_webhook_triggers_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_org": { + "name": "IDX_cloud_agent_webhook_triggers_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_active": { + "name": "IDX_cloud_agent_webhook_triggers_active", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_webhook_triggers_profile": { + "name": "IDX_cloud_agent_webhook_triggers_profile", + "columns": [ + { + "expression": "profile_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_webhook_triggers_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_webhook_triggers_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_organization_id_organizations_id_fk": { + "name": "cloud_agent_webhook_triggers_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_kiloclaw_instance_id_kiloclaw_instances_id_fk": { + "name": "cloud_agent_webhook_triggers_kiloclaw_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "kiloclaw_instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cloud_agent_webhook_triggers_profile_id_agent_environment_profiles_id_fk": { + "name": "cloud_agent_webhook_triggers_profile_id_agent_environment_profiles_id_fk", + "tableFrom": "cloud_agent_webhook_triggers", + "tableTo": "agent_environment_profiles", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_cloud_agent_webhook_triggers_owner": { + "name": "CHK_cloud_agent_webhook_triggers_owner", + "value": "(\n (\"cloud_agent_webhook_triggers\".\"user_id\" IS NOT NULL AND \"cloud_agent_webhook_triggers\".\"organization_id\" IS NULL) OR\n (\"cloud_agent_webhook_triggers\".\"user_id\" IS NULL AND \"cloud_agent_webhook_triggers\".\"organization_id\" IS NOT NULL)\n )" + }, + "CHK_cloud_agent_webhook_triggers_cloud_agent_fields": { + "name": "CHK_cloud_agent_webhook_triggers_cloud_agent_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"target_type\" != 'cloud_agent' OR\n \"cloud_agent_webhook_triggers\".\"github_repo\" IS NOT NULL\n )" + }, + "CHK_cloud_agent_webhook_triggers_kiloclaw_fields": { + "name": "CHK_cloud_agent_webhook_triggers_kiloclaw_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"target_type\" != 'kiloclaw_chat' OR\n \"cloud_agent_webhook_triggers\".\"kiloclaw_instance_id\" IS NOT NULL\n )" + }, + "CHK_cloud_agent_webhook_triggers_scheduled_fields": { + "name": "CHK_cloud_agent_webhook_triggers_scheduled_fields", + "value": "(\n \"cloud_agent_webhook_triggers\".\"activation_mode\" != 'scheduled' OR\n \"cloud_agent_webhook_triggers\".\"cron_expression\" IS NOT NULL\n )" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_workspace_folders": { + "name": "cloud_agent_workspace_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_cloud_agent_workspace_folders_owner_scope_order": { + "name": "IDX_cloud_agent_workspace_folders_owner_scope_order", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_workspace_folders_kilo_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_workspace_folders_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_workspace_folders", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cloud_agent_workspace_folders_organization_id_organizations_id_fk": { + "name": "cloud_agent_workspace_folders_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_workspace_folders", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_workspace_folders_color_check": { + "name": "cloud_agent_workspace_folders_color_check", + "value": "\"cloud_agent_workspace_folders\".\"color\" IN ('default', 'red', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple')" + }, + "cloud_agent_workspace_folders_name_check": { + "name": "cloud_agent_workspace_folders_name_check", + "value": "char_length(btrim(\"cloud_agent_workspace_folders\".\"name\")) BETWEEN 1 AND 200 AND \"cloud_agent_workspace_folders\".\"name\" = btrim(\"cloud_agent_workspace_folders\".\"name\")" + }, + "cloud_agent_workspace_folders_position_check": { + "name": "cloud_agent_workspace_folders_position_check", + "value": "\"cloud_agent_workspace_folders\".\"position\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.cloud_agent_worktrees": { + "name": "cloud_agent_worktrees", + "schema": "", + "columns": { + "worktree_id": { + "name": "worktree_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "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()" + }, + "deletion_started_at": { + "name": "deletion_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deletion_completed_at": { + "name": "deletion_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "runtime_locations": { + "name": "runtime_locations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "deletion_manifest": { + "name": "deletion_manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted_session_ids": { + "name": "deleted_session_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + } + }, + "indexes": { + "IDX_cloud_agent_worktrees_owner_scope": { + "name": "IDX_cloud_agent_worktrees_owner_scope", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_cloud_agent_worktrees_folder_id": { + "name": "IDX_cloud_agent_worktrees_folder_id", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"cloud_agent_worktrees\".\"folder_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cloud_agent_worktrees_kilo_user_id_kilocode_users_id_fk": { + "name": "cloud_agent_worktrees_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_agent_worktrees", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cloud_agent_worktrees_organization_id_organizations_id_fk": { + "name": "cloud_agent_worktrees_organization_id_organizations_id_fk", + "tableFrom": "cloud_agent_worktrees", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "cloud_agent_worktrees_folder_id_cloud_agent_workspace_folders_id_fk": { + "name": "cloud_agent_worktrees_folder_id_cloud_agent_workspace_folders_id_fk", + "tableFrom": "cloud_agent_worktrees", + "tableTo": "cloud_agent_workspace_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_agent_worktrees_deletion_check": { + "name": "cloud_agent_worktrees_deletion_check", + "value": "\"cloud_agent_worktrees\".\"deletion_completed_at\" IS NULL OR (\"cloud_agent_worktrees\".\"deletion_started_at\" IS NOT NULL AND \"cloud_agent_worktrees\".\"name\" IS NULL AND \"cloud_agent_worktrees\".\"deletion_manifest\" IS NULL AND \"cloud_agent_worktrees\".\"runtime_locations\" = '[]'::jsonb)" + } + }, + "isRLSEnabled": false + }, + "public.cloud_billing_sku": { + "name": "cloud_billing_sku", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rate_cents_per_unit": { + "name": "rate_cents_per_unit", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": true + }, + "accepts_new_usage": { + "name": "accepts_new_usage", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cloud_billing_sku_created_by_user_id_kilocode_users_id_fk": { + "name": "cloud_billing_sku_created_by_user_id_kilocode_users_id_fk", + "tableFrom": "cloud_billing_sku", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "cloud_billing_sku_id_format": { + "name": "cloud_billing_sku_id_format", + "value": "\"cloud_billing_sku\".\"id\" ~ '^[a-z0-9][a-z0-9-]{2,79}$'" + }, + "cloud_billing_sku_name_nonempty": { + "name": "cloud_billing_sku_name_nonempty", + "value": "length(btrim(\"cloud_billing_sku\".\"name\")) > 0" + }, + "cloud_billing_sku_rate_positive": { + "name": "cloud_billing_sku_rate_positive", + "value": "\"cloud_billing_sku\".\"rate_cents_per_unit\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.code_indexing_manifest": { + "name": "code_indexing_manifest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_lines": { + "name": "total_lines", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_ai_lines": { + "name": "total_ai_lines", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_code_indexing_manifest_organization_id": { + "name": "IDX_code_indexing_manifest_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_kilo_user_id": { + "name": "IDX_code_indexing_manifest_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_project_id": { + "name": "IDX_code_indexing_manifest_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_git_branch": { + "name": "IDX_code_indexing_manifest_git_branch", + "columns": [ + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_manifest_created_at": { + "name": "IDX_code_indexing_manifest_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_indexing_manifest_kilo_user_id_kilocode_users_id_fk": { + "name": "code_indexing_manifest_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "code_indexing_manifest", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_indexing_manifest_org_user_project_hash_branch": { + "name": "UQ_code_indexing_manifest_org_user_project_hash_branch", + "nullsNotDistinct": true, + "columns": [ + "organization_id", + "kilo_user_id", + "project_id", + "file_path", + "git_branch" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.code_indexing_search": { + "name": "code_indexing_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "query": { + "name": "query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_code_indexing_search_organization_id": { + "name": "IDX_code_indexing_search_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_kilo_user_id": { + "name": "IDX_code_indexing_search_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_project_id": { + "name": "IDX_code_indexing_search_project_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_code_indexing_search_created_at": { + "name": "IDX_code_indexing_search_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_indexing_search_kilo_user_id_kilocode_users_id_fk": { + "name": "code_indexing_search_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "code_indexing_search", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.code_review_analytics_findings": { + "name": "code_review_analytics_findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "analytics_result_id": { + "name": "analytics_result_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "security_class": { + "name": "security_class", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "code_review_analytics_findings_analytics_result_id_code_review_analytics_results_id_fk": { + "name": "code_review_analytics_findings_analytics_result_id_code_review_analytics_results_id_fk", + "tableFrom": "code_review_analytics_findings", + "tableTo": "code_review_analytics_results", + "columnsFrom": [ + "analytics_result_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_analytics_findings_result_ordinal": { + "name": "UQ_code_review_analytics_findings_result_ordinal", + "nullsNotDistinct": false, + "columns": [ + "analytics_result_id", + "ordinal" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_analytics_findings_severity_check": { + "name": "code_review_analytics_findings_severity_check", + "value": "\"code_review_analytics_findings\".\"severity\" IN ('critical', 'warning', 'suggestion')" + }, + "code_review_analytics_findings_category_check": { + "name": "code_review_analytics_findings_category_check", + "value": "\"code_review_analytics_findings\".\"category\" IN ('security', 'correctness', 'reliability', 'data_integrity', 'performance', 'compatibility', 'maintainability', 'test_quality', 'documentation', 'accessibility', 'other')" + }, + "code_review_analytics_findings_security_class_check": { + "name": "code_review_analytics_findings_security_class_check", + "value": "\"code_review_analytics_findings\".\"security_class\" IN ('auth_access', 'injection', 'data_protection', 'request_resource_boundary', 'deserialization_object_integrity', 'dependency_supply_chain', 'memory_safety', 'availability', 'concurrency', 'security_configuration', 'other')" + }, + "code_review_analytics_findings_ordinal_check": { + "name": "code_review_analytics_findings_ordinal_check", + "value": "\"code_review_analytics_findings\".\"ordinal\" >= 0" + }, + "code_review_analytics_findings_security_class_presence_check": { + "name": "code_review_analytics_findings_security_class_presence_check", + "value": "(\n (\"code_review_analytics_findings\".\"category\" = 'security' AND \"code_review_analytics_findings\".\"security_class\" IS NOT NULL) OR\n (\"code_review_analytics_findings\".\"category\" <> 'security' AND \"code_review_analytics_findings\".\"security_class\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_analytics_results": { + "name": "code_review_analytics_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code_review_id": { + "name": "code_review_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_attempt_id": { + "name": "source_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "capture_status": { + "name": "capture_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "taxonomy_version": { + "name": "taxonomy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "change_type": { + "name": "change_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_level": { + "name": "impact_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "complexity_level": { + "name": "complexity_level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "classification_confidence": { + "name": "classification_confidence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finalized_at": { + "name": "finalized_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": { + "idx_code_review_analytics_results_source_attempt_id": { + "name": "idx_code_review_analytics_results_source_attempt_id", + "columns": [ + { + "expression": "source_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_analytics_results_finalized_at": { + "name": "idx_code_review_analytics_results_finalized_at", + "columns": [ + { + "expression": "finalized_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_analytics_results_code_review_id_cloud_agent_code_reviews_id_fk": { + "name": "code_review_analytics_results_code_review_id_cloud_agent_code_reviews_id_fk", + "tableFrom": "code_review_analytics_results", + "tableTo": "cloud_agent_code_reviews", + "columnsFrom": [ + "code_review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_analytics_results_source_attempt_id_cloud_agent_code_review_attempts_id_fk": { + "name": "code_review_analytics_results_source_attempt_id_cloud_agent_code_review_attempts_id_fk", + "tableFrom": "code_review_analytics_results", + "tableTo": "cloud_agent_code_review_attempts", + "columnsFrom": [ + "source_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_analytics_results_code_review_id": { + "name": "UQ_code_review_analytics_results_code_review_id", + "nullsNotDistinct": false, + "columns": [ + "code_review_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_analytics_results_capture_status_check": { + "name": "code_review_analytics_results_capture_status_check", + "value": "\"code_review_analytics_results\".\"capture_status\" IN ('captured', 'missing', 'invalid', 'omitted')" + }, + "code_review_analytics_results_change_type_check": { + "name": "code_review_analytics_results_change_type_check", + "value": "\"code_review_analytics_results\".\"change_type\" IN ('bug_fix', 'feature', 'refactor', 'maintenance', 'dependency', 'test', 'documentation', 'mixed', 'other')" + }, + "code_review_analytics_results_impact_level_check": { + "name": "code_review_analytics_results_impact_level_check", + "value": "\"code_review_analytics_results\".\"impact_level\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_complexity_level_check": { + "name": "code_review_analytics_results_complexity_level_check", + "value": "\"code_review_analytics_results\".\"complexity_level\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_classification_confidence_check": { + "name": "code_review_analytics_results_classification_confidence_check", + "value": "\"code_review_analytics_results\".\"classification_confidence\" IN ('low', 'medium', 'high')" + }, + "code_review_analytics_results_classification_presence_check": { + "name": "code_review_analytics_results_classification_presence_check", + "value": "(\n (\n \"code_review_analytics_results\".\"capture_status\" = 'captured'\n AND \"code_review_analytics_results\".\"change_type\" IS NOT NULL\n AND \"code_review_analytics_results\".\"impact_level\" IS NOT NULL\n AND \"code_review_analytics_results\".\"complexity_level\" IS NOT NULL\n AND \"code_review_analytics_results\".\"classification_confidence\" IS NOT NULL\n ) OR (\n \"code_review_analytics_results\".\"capture_status\" <> 'captured'\n AND \"code_review_analytics_results\".\"change_type\" IS NULL\n AND \"code_review_analytics_results\".\"impact_level\" IS NULL\n AND \"code_review_analytics_results\".\"complexity_level\" IS NULL\n AND \"code_review_analytics_results\".\"classification_confidence\" IS NULL\n )\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_feedback_events": { + "name": "code_review_feedback_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "kilo_comment_id": { + "name": "kilo_comment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_excerpt": { + "name": "reply_excerpt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_comment_excerpt": { + "name": "kilo_comment_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dedupe_hash": { + "name": "dedupe_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_code_review_feedback_events_owned_by_org_id": { + "name": "idx_code_review_feedback_events_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_owned_by_user_id": { + "name": "idx_code_review_feedback_events_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_platform_repo": { + "name": "idx_code_review_feedback_events_platform_repo", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_feedback_events_created_at": { + "name": "idx_code_review_feedback_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_feedback_events_owned_by_organization_id_organizations_id_fk": { + "name": "code_review_feedback_events_owned_by_organization_id_organizations_id_fk", + "tableFrom": "code_review_feedback_events", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_feedback_events_owned_by_user_id_kilocode_users_id_fk": { + "name": "code_review_feedback_events_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "code_review_feedback_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_code_review_feedback_events_dedupe_hash": { + "name": "UQ_code_review_feedback_events_dedupe_hash", + "nullsNotDistinct": false, + "columns": [ + "dedupe_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "code_review_feedback_events_owner_check": { + "name": "code_review_feedback_events_owner_check", + "value": "(\n (\"code_review_feedback_events\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_feedback_events\".\"owned_by_organization_id\" IS NULL) OR\n (\"code_review_feedback_events\".\"owned_by_user_id\" IS NULL AND \"code_review_feedback_events\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.code_review_memory_proposals": { + "name": "code_review_memory_proposals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_markdown": { + "name": "proposed_markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "positive_count": { + "name": "positive_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "negative_count": { + "name": "negative_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "neutral_count": { + "name": "neutral_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "change_request_url": { + "name": "change_request_url", + "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": { + "idx_code_review_memory_proposals_owned_by_org_id": { + "name": "idx_code_review_memory_proposals_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_owned_by_user_id": { + "name": "idx_code_review_memory_proposals_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_platform_repo_status": { + "name": "idx_code_review_memory_proposals_platform_repo_status", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_code_review_memory_proposals_updated_at": { + "name": "idx_code_review_memory_proposals_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_code_review_memory_proposals_org_active_scope": { + "name": "UQ_code_review_memory_proposals_org_active_scope", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"code_review_memory_proposals\".\"owned_by_organization_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"status\" IN ('open', 'edited', 'opening_change_request')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_code_review_memory_proposals_user_active_scope": { + "name": "UQ_code_review_memory_proposals_user_active_scope", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"code_review_memory_proposals\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"status\" IN ('open', 'edited', 'opening_change_request')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "code_review_memory_proposals_owned_by_organization_id_organizations_id_fk": { + "name": "code_review_memory_proposals_owned_by_organization_id_organizations_id_fk", + "tableFrom": "code_review_memory_proposals", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "code_review_memory_proposals_owned_by_user_id_kilocode_users_id_fk": { + "name": "code_review_memory_proposals_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "code_review_memory_proposals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "code_review_memory_proposals_owner_check": { + "name": "code_review_memory_proposals_owner_check", + "value": "(\n (\"code_review_memory_proposals\".\"owned_by_user_id\" IS NOT NULL AND \"code_review_memory_proposals\".\"owned_by_organization_id\" IS NULL) OR\n (\"code_review_memory_proposals\".\"owned_by_user_id\" IS NULL AND \"code_review_memory_proposals\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_availability_intents": { + "name": "coding_plan_availability_intents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_availability_intents_user_plan": { + "name": "UQ_coding_plan_availability_intents_user_plan", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_availability_intents_plan": { + "name": "IDX_coding_plan_availability_intents_plan", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_availability_intents_user_id_kilocode_users_id_fk": { + "name": "coding_plan_availability_intents_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_availability_intents", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coding_plan_key_inventory": { + "name": "coding_plan_key_inventory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upstream_plan_id": { + "name": "upstream_plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "upstream_usage_id": { + "name": "upstream_usage_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "credential_fingerprint": { + "name": "credential_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "assigned_to_user_id": { + "name": "assigned_to_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_requested_at": { + "name": "revocation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_attempt_count": { + "name": "revocation_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_revocation_error": { + "name": "last_revocation_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": { + "UQ_coding_plan_key_inv_fingerprint": { + "name": "UQ_coding_plan_key_inv_fingerprint", + "columns": [ + { + "expression": "credential_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_coding_plan_key_inv_provider_usage_id": { + "name": "UQ_coding_plan_key_inv_provider_usage_id", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "upstream_usage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_key_inventory\".\"upstream_usage_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_key_inv_plan_status": { + "name": "IDX_coding_plan_key_inv_plan_status", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_key_inv_available": { + "name": "IDX_coding_plan_key_inv_available", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"coding_plan_key_inventory\".\"status\" = 'available'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_key_inventory_assigned_to_user_id_kilocode_users_id_fk": { + "name": "coding_plan_key_inventory_assigned_to_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_key_inventory", + "tableTo": "kilocode_users", + "columnsFrom": [ + "assigned_to_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_key_inventory_status_check": { + "name": "coding_plan_key_inventory_status_check", + "value": "\"coding_plan_key_inventory\".\"status\" IN ('available', 'assigned', 'revocation_pending', 'revoked', 'revocation_failed')" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_subscriptions": { + "name": "coding_plan_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_inventory_id": { + "name": "key_inventory_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "installed_byok_key_id": { + "name": "installed_byok_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "billing_period_days": { + "name": "billing_period_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "credit_renewal_at": { + "name": "credit_renewal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "past_due_started_at": { + "name": "past_due_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payment_grace_expires_at": { + "name": "payment_grace_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_attempted_for_due": { + "name": "auto_top_up_attempted_for_due", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_reason": { + "name": "cancellation_reason", + "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": { + "UQ_coding_plan_sub_live_user_plan": { + "name": "UQ_coding_plan_sub_live_user_plan", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_coding_plan_sub_live_user_provider": { + "name": "UQ_coding_plan_sub_live_user_provider", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_status": { + "name": "IDX_coding_plan_sub_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_renewal": { + "name": "IDX_coding_plan_sub_renewal", + "columns": [ + { + "expression": "credit_renewal_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_sub_inventory": { + "name": "IDX_coding_plan_sub_inventory", + "columns": [ + { + "expression": "key_inventory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_subscriptions_user_id_kilocode_users_id_fk": { + "name": "coding_plan_subscriptions_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_subscriptions_key_inventory_id_coding_plan_key_inventory_id_fk": { + "name": "coding_plan_subscriptions_key_inventory_id_coding_plan_key_inventory_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "coding_plan_key_inventory", + "columnsFrom": [ + "key_inventory_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "coding_plan_subscriptions_installed_byok_key_id_byok_api_keys_id_fk": { + "name": "coding_plan_subscriptions_installed_byok_key_id_byok_api_keys_id_fk", + "tableFrom": "coding_plan_subscriptions", + "tableTo": "byok_api_keys", + "columnsFrom": [ + "installed_byok_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_subscriptions_status_check": { + "name": "coding_plan_subscriptions_status_check", + "value": "\"coding_plan_subscriptions\".\"status\" IN ('active', 'past_due', 'canceled')" + }, + "coding_plan_subscriptions_live_access_check": { + "name": "coding_plan_subscriptions_live_access_check", + "value": "\"coding_plan_subscriptions\".\"status\" = 'canceled' OR \"coding_plan_subscriptions\".\"key_inventory_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.coding_plan_terms": { + "name": "coding_plan_terms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_coding_plan_terms_request": { + "name": "UQ_coding_plan_terms_request", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_coding_plan_terms_subscription": { + "name": "IDX_coding_plan_terms_subscription", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "coding_plan_terms_subscription_id_coding_plan_subscriptions_id_fk": { + "name": "coding_plan_terms_subscription_id_coding_plan_subscriptions_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "coding_plan_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_terms_user_id_kilocode_users_id_fk": { + "name": "coding_plan_terms_user_id_kilocode_users_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "coding_plan_terms_credit_transaction_id_credit_transactions_id_fk": { + "name": "coding_plan_terms_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "coding_plan_terms", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "coding_plan_terms_kind_check": { + "name": "coding_plan_terms_kind_check", + "value": "\"coding_plan_terms\".\"kind\" IN ('activation', 'extension', 'renewal')" + } + }, + "isRLSEnabled": false + }, + "public.compute_usage_charge": { + "name": "compute_usage_charge", + "schema": "", + "columns": { + "usage_source": { + "name": "usage_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_source_id": { + "name": "usage_source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cloud_billing_sku_id": { + "name": "cloud_billing_sku_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": true + }, + "settled_quantity_after": { + "name": "settled_quantity_after", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": false + }, + "rate_cents_per_unit": { + "name": "rate_cents_per_unit", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_compute_usage_charge_user_created": { + "name": "IDX_compute_usage_charge_user_created", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_compute_usage_charge_organization_created": { + "name": "IDX_compute_usage_charge_organization_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "compute_usage_charge_user_id_kilocode_users_id_fk": { + "name": "compute_usage_charge_user_id_kilocode_users_id_fk", + "tableFrom": "compute_usage_charge", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "compute_usage_charge_organization_id_organizations_id_fk": { + "name": "compute_usage_charge_organization_id_organizations_id_fk", + "tableFrom": "compute_usage_charge", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "compute_usage_charge_cloud_billing_sku_id_cloud_billing_sku_id_fk": { + "name": "compute_usage_charge_cloud_billing_sku_id_cloud_billing_sku_id_fk", + "tableFrom": "compute_usage_charge", + "tableTo": "cloud_billing_sku", + "columnsFrom": [ + "cloud_billing_sku_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "compute_usage_charge_usage_source_usage_source_id_created_at_pk": { + "name": "compute_usage_charge_usage_source_usage_source_id_created_at_pk", + "columns": [ + "usage_source", + "usage_source_id", + "created_at" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "compute_usage_charge_exactly_one_payer": { + "name": "compute_usage_charge_exactly_one_payer", + "value": "(\"compute_usage_charge\".\"user_id\" IS NULL) <> (\"compute_usage_charge\".\"organization_id\" IS NULL)" + }, + "compute_usage_charge_quantity_positive": { + "name": "compute_usage_charge_quantity_positive", + "value": "\"compute_usage_charge\".\"quantity\" > 0" + }, + "compute_usage_charge_settled_quantity_positive": { + "name": "compute_usage_charge_settled_quantity_positive", + "value": "\"compute_usage_charge\".\"settled_quantity_after\" IS NULL OR \"compute_usage_charge\".\"settled_quantity_after\" > 0" + }, + "compute_usage_charge_rate_positive": { + "name": "compute_usage_charge_rate_positive", + "value": "\"compute_usage_charge\".\"rate_cents_per_unit\" > 0" + }, + "compute_usage_charge_amount_positive": { + "name": "compute_usage_charge_amount_positive", + "value": "\"compute_usage_charge\".\"amount_microdollars\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.container_usage_interval": { + "name": "container_usage_interval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_epoch_ms": { + "name": "start_epoch_ms", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cloud_billing_sku_id": { + "name": "cloud_billing_sku_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_fingerprint": { + "name": "context_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_heartbeat_seq": { + "name": "last_heartbeat_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "confirmed_seconds": { + "name": "confirmed_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_mode": { + "name": "billing_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'shadow'" + }, + "rate_cents_per_unit": { + "name": "rate_cents_per_unit", + "type": "numeric(24, 12)", + "primaryKey": false, + "notNull": false + }, + "settled_billable_seconds": { + "name": "settled_billable_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "close_reason": { + "name": "close_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "final_stop_seq": { + "name": "final_stop_seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_container_usage_interval_sweep": { + "name": "IDX_container_usage_interval_sweep", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_container_usage_interval_subject_started": { + "name": "IDX_container_usage_interval_subject_started", + "columns": [ + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_container_usage_interval_open_subject": { + "name": "IDX_container_usage_interval_open_subject", + "columns": [ + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"container_usage_interval\".\"status\" = 'open'", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_container_usage_interval_single_open": { + "name": "UQ_container_usage_interval_single_open", + "columns": [ + { + "expression": "service", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"container_usage_interval\".\"status\" = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_usage_interval_cloud_billing_sku_id_cloud_billing_sku_id_fk": { + "name": "container_usage_interval_cloud_billing_sku_id_cloud_billing_sku_id_fk", + "tableFrom": "container_usage_interval", + "tableTo": "cloud_billing_sku", + "columnsFrom": [ + "cloud_billing_sku_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "container_usage_interval_subject_type": { + "name": "container_usage_interval_subject_type", + "value": "\"container_usage_interval\".\"subject_type\" IN ('user', 'org')" + }, + "container_usage_interval_actor_type": { + "name": "container_usage_interval_actor_type", + "value": "\"container_usage_interval\".\"actor_type\" IN ('user', 'bot')" + }, + "container_usage_interval_context_fingerprint": { + "name": "container_usage_interval_context_fingerprint", + "value": "\"container_usage_interval\".\"context_fingerprint\" ~ '^[a-f0-9]{64}$'" + }, + "container_usage_interval_attribution": { + "name": "container_usage_interval_attribution", + "value": "\"container_usage_interval\".\"actor_type\" = 'bot' OR (\"container_usage_interval\".\"actor_type\" = 'user' AND (\"container_usage_interval\".\"subject_type\" <> 'user' OR \"container_usage_interval\".\"actor_id\" = \"container_usage_interval\".\"subject_id\"))" + }, + "container_usage_interval_status": { + "name": "container_usage_interval_status", + "value": "\"container_usage_interval\".\"status\" IN ('open', 'closed')" + }, + "container_usage_interval_billing_mode": { + "name": "container_usage_interval_billing_mode", + "value": "\"container_usage_interval\".\"billing_mode\" IN ('shadow', 'paid')" + }, + "container_usage_interval_paid_rate": { + "name": "container_usage_interval_paid_rate", + "value": "(\"container_usage_interval\".\"billing_mode\" = 'shadow' AND \"container_usage_interval\".\"rate_cents_per_unit\" IS NULL) OR (\"container_usage_interval\".\"billing_mode\" = 'paid' AND \"container_usage_interval\".\"rate_cents_per_unit\" > 0)" + }, + "container_usage_interval_open_closed_shape": { + "name": "container_usage_interval_open_closed_shape", + "value": "(\"container_usage_interval\".\"status\" = 'open' AND \"container_usage_interval\".\"stopped_at\" IS NULL AND \"container_usage_interval\".\"close_reason\" IS NULL) OR (\"container_usage_interval\".\"status\" = 'closed' AND \"container_usage_interval\".\"stopped_at\" IS NOT NULL AND \"container_usage_interval\".\"close_reason\" IS NOT NULL)" + }, + "container_usage_interval_time_order": { + "name": "container_usage_interval_time_order", + "value": "\"container_usage_interval\".\"last_seen_at\" >= \"container_usage_interval\".\"started_at\" AND (\"container_usage_interval\".\"stopped_at\" IS NULL OR (\"container_usage_interval\".\"stopped_at\" >= \"container_usage_interval\".\"started_at\" AND \"container_usage_interval\".\"stopped_at\" <= \"container_usage_interval\".\"last_seen_at\"))" + }, + "container_usage_interval_last_heartbeat_seq_nonnegative": { + "name": "container_usage_interval_last_heartbeat_seq_nonnegative", + "value": "\"container_usage_interval\".\"last_heartbeat_seq\" >= 0" + }, + "container_usage_interval_confirmed_seconds_nonnegative": { + "name": "container_usage_interval_confirmed_seconds_nonnegative", + "value": "\"container_usage_interval\".\"confirmed_seconds\" >= 0" + }, + "container_usage_interval_settled_billable_seconds_nonnegative": { + "name": "container_usage_interval_settled_billable_seconds_nonnegative", + "value": "\"container_usage_interval\".\"settled_billable_seconds\" >= 0 AND \"container_usage_interval\".\"settled_billable_seconds\" <= \"container_usage_interval\".\"confirmed_seconds\"" + }, + "container_usage_interval_final_stop_seq_positive": { + "name": "container_usage_interval_final_stop_seq_positive", + "value": "\"container_usage_interval\".\"final_stop_seq\" IS NULL OR \"container_usage_interval\".\"final_stop_seq\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.container_usage_segment": { + "name": "container_usage_segment", + "schema": "", + "columns": { + "interval_id": { + "name": "interval_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_seconds": { + "name": "reported_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "usage_seconds": { + "name": "usage_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_container_usage_segment_received": { + "name": "IDX_container_usage_segment_received", + "columns": [ + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_usage_segment_interval_id_container_usage_interval_id_fk": { + "name": "container_usage_segment_interval_id_container_usage_interval_id_fk", + "tableFrom": "container_usage_segment", + "tableTo": "container_usage_interval", + "columnsFrom": [ + "interval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "container_usage_segment_interval_id_seq_pk": { + "name": "container_usage_segment_interval_id_seq_pk", + "columns": [ + "interval_id", + "seq" + ] + } + }, + "uniqueConstraints": { + "container_usage_segment_idempotency_key_unique": { + "name": "container_usage_segment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "container_usage_segment_seq_positive": { + "name": "container_usage_segment_seq_positive", + "value": "\"container_usage_segment\".\"seq\" > 0" + }, + "container_usage_segment_reported_seconds_nonnegative": { + "name": "container_usage_segment_reported_seconds_nonnegative", + "value": "\"container_usage_segment\".\"reported_seconds\" >= 0" + }, + "container_usage_segment_usage_seconds_nonnegative": { + "name": "container_usage_segment_usage_seconds_nonnegative", + "value": "\"container_usage_segment\".\"usage_seconds\" >= 0" + }, + "container_usage_segment_usage_within_reported": { + "name": "container_usage_segment_usage_within_reported", + "value": "\"container_usage_segment\".\"usage_seconds\" <= \"container_usage_segment\".\"reported_seconds\"" + } + }, + "isRLSEnabled": false + }, + "public.content_moderation_reports": { + "name": "content_moderation_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_json": { + "name": "context_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "receipt_id": { + "name": "receipt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "triage_status": { + "name": "triage_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "appeal_status": { + "name": "appeal_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "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": { + "IDX_content_moderation_reports_user_created": { + "name": "IDX_content_moderation_reports_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_content_moderation_reports_target": { + "name": "IDX_content_moderation_reports_target", + "columns": [ + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "content_moderation_reports_receipt_id_unique": { + "name": "content_moderation_reports_receipt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "receipt_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_contributors": { + "name": "contributor_champion_contributors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_profile_url": { + "name": "github_profile_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "first_contribution_at": { + "name": "first_contribution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_contribution_at": { + "name": "last_contribution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "all_time_contributions": { + "name": "all_time_contributions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "manual_email": { + "name": "manual_email", + "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": { + "IDX_contributor_champion_contributors_last_contribution_at": { + "name": "IDX_contributor_champion_contributors_last_contribution_at", + "columns": [ + { + "expression": "last_contribution_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_contributors_manual_email": { + "name": "IDX_contributor_champion_contributors_manual_email", + "columns": [ + { + "expression": "manual_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_contributors_github_login": { + "name": "UQ_contributor_champion_contributors_github_login", + "nullsNotDistinct": false, + "columns": [ + "github_login" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_events": { + "name": "contributor_champion_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "contributor_id": { + "name": "contributor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_pr_number": { + "name": "github_pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "github_pr_url": { + "name": "github_pr_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_pr_title": { + "name": "github_pr_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_author_login": { + "name": "github_author_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_author_email": { + "name": "github_author_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_at": { + "name": "merged_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": { + "IDX_contributor_champion_events_contributor_id": { + "name": "IDX_contributor_champion_events_contributor_id", + "columns": [ + { + "expression": "contributor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_events_merged_at": { + "name": "IDX_contributor_champion_events_merged_at", + "columns": [ + { + "expression": "merged_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_events_author_email": { + "name": "IDX_contributor_champion_events_author_email", + "columns": [ + { + "expression": "github_author_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contributor_champion_events_contributor_id_contributor_champion_contributors_id_fk": { + "name": "contributor_champion_events_contributor_id_contributor_champion_contributors_id_fk", + "tableFrom": "contributor_champion_events", + "tableTo": "contributor_champion_contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_events_repo_pr": { + "name": "UQ_contributor_champion_events_repo_pr", + "nullsNotDistinct": false, + "columns": [ + "repo_full_name", + "github_pr_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contributor_champion_memberships": { + "name": "contributor_champion_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "contributor_id": { + "name": "contributor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "selected_tier": { + "name": "selected_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrolled_tier": { + "name": "enrolled_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrolled_at": { + "name": "enrolled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credit_amount_microdollars": { + "name": "credit_amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credits_last_granted_at": { + "name": "credits_last_granted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credits_window_granted_microdollars": { + "name": "credits_window_granted_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "linked_kilo_user_id": { + "name": "linked_kilo_user_id", + "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": { + "IDX_contributor_champion_memberships_credits_due": { + "name": "IDX_contributor_champion_memberships_credits_due", + "columns": [ + { + "expression": "credits_last_granted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"contributor_champion_memberships\".\"enrolled_tier\" IS NOT NULL AND \"contributor_champion_memberships\".\"credit_amount_microdollars\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_contributor_champion_memberships_linked_kilo_user_id": { + "name": "IDX_contributor_champion_memberships_linked_kilo_user_id", + "columns": [ + { + "expression": "linked_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contributor_champion_memberships_contributor_id_contributor_champion_contributors_id_fk": { + "name": "contributor_champion_memberships_contributor_id_contributor_champion_contributors_id_fk", + "tableFrom": "contributor_champion_memberships", + "tableTo": "contributor_champion_contributors", + "columnsFrom": [ + "contributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "contributor_champion_memberships_linked_kilo_user_id_kilocode_users_id_fk": { + "name": "contributor_champion_memberships_linked_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "contributor_champion_memberships", + "tableTo": "kilocode_users", + "columnsFrom": [ + "linked_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_contributor_champion_memberships_contributor_id": { + "name": "UQ_contributor_champion_memberships_contributor_id", + "nullsNotDistinct": false, + "columns": [ + "contributor_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "contributor_champion_memberships_selected_tier_check": { + "name": "contributor_champion_memberships_selected_tier_check", + "value": "\"contributor_champion_memberships\".\"selected_tier\" IS NULL OR \"contributor_champion_memberships\".\"selected_tier\" IN ('contributor', 'ambassador', 'champion')" + }, + "contributor_champion_memberships_enrolled_tier_check": { + "name": "contributor_champion_memberships_enrolled_tier_check", + "value": "\"contributor_champion_memberships\".\"enrolled_tier\" IS NULL OR \"contributor_champion_memberships\".\"enrolled_tier\" IN ('contributor', 'ambassador', 'champion')" + } + }, + "isRLSEnabled": false + }, + "public.contributor_champion_sync_state": { + "name": "contributor_champion_sync_state", + "schema": "", + "columns": { + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_merged_at": { + "name": "last_merged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credit_campaigns": { + "name": "credit_campaigns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credit_category": { + "name": "credit_category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credit_expiry_hours": { + "name": "credit_expiry_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_ends_at": { + "name": "campaign_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_redemptions_allowed": { + "name": "total_redemptions_allowed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_credit_campaigns_slug": { + "name": "UQ_credit_campaigns_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_credit_campaigns_credit_category": { + "name": "UQ_credit_campaigns_credit_category", + "columns": [ + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credit_campaigns_slug_format_check": { + "name": "credit_campaigns_slug_format_check", + "value": "\"credit_campaigns\".\"slug\" ~ '^[a-z0-9-]{5,40}$'" + }, + "credit_campaigns_amount_positive_check": { + "name": "credit_campaigns_amount_positive_check", + "value": "\"credit_campaigns\".\"amount_microdollars\" > 0" + }, + "credit_campaigns_credit_expiry_hours_positive_check": { + "name": "credit_campaigns_credit_expiry_hours_positive_check", + "value": "\"credit_campaigns\".\"credit_expiry_hours\" IS NULL OR \"credit_campaigns\".\"credit_expiry_hours\" > 0" + }, + "credit_campaigns_total_redemptions_allowed_positive_check": { + "name": "credit_campaigns_total_redemptions_allowed_positive_check", + "value": "\"credit_campaigns\".\"total_redemptions_allowed\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.credit_transactions": { + "name": "credit_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_microdollars": { + "name": "amount_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expiration_baseline_microdollars_used": { + "name": "expiration_baseline_microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "original_baseline_microdollars_used": { + "name": "original_baseline_microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_transaction_id": { + "name": "original_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_id": { + "name": "stripe_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coinbase_credit_block_id": { + "name": "coinbase_credit_block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credit_category": { + "name": "credit_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_date": { + "name": "expiry_date", + "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()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "check_category_uniqueness": { + "name": "check_category_uniqueness", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_credit_transactions_created_at": { + "name": "IDX_credit_transactions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_is_free": { + "name": "IDX_credit_transactions_is_free", + "columns": [ + { + "expression": "is_free", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_kilo_user_id": { + "name": "IDX_credit_transactions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_credit_category": { + "name": "IDX_credit_transactions_credit_category", + "columns": [ + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_stripe_payment_id": { + "name": "IDX_credit_transactions_stripe_payment_id", + "columns": [ + { + "expression": "stripe_payment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_original_transaction_id": { + "name": "IDX_credit_transactions_original_transaction_id", + "columns": [ + { + "expression": "original_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_coinbase_credit_block_id": { + "name": "IDX_credit_transactions_coinbase_credit_block_id", + "columns": [ + { + "expression": "coinbase_credit_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_organization_id": { + "name": "IDX_credit_transactions_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_credit_transactions_unique_category": { + "name": "IDX_credit_transactions_unique_category", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credit_category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credit_transactions\".\"check_category_uniqueness\" = TRUE", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credit_transactions_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "credit_transactions_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "credit_transactions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_llm2": { + "name": "custom_llm2", + "schema": "", + "columns": { + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "definition": { + "name": "definition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deleted_user_email_tombstones": { + "name": "deleted_user_email_tombstones", + "schema": "", + "columns": { + "normalized_email_hash": { + "name": "normalized_email_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_auth_requests": { + "name": "device_auth_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requesting_client_id": { + "name": "requesting_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requesting_client_name": { + "name": "requesting_client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requesting_redirect_origin": { + "name": "requesting_redirect_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_confirmed_at": { + "name": "client_confirmed_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": { + "UQ_device_auth_requests_code": { + "name": "UQ_device_auth_requests_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_status": { + "name": "IDX_device_auth_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_expires_at": { + "name": "IDX_device_auth_requests_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_auth_requests_kilo_user_id": { + "name": "IDX_device_auth_requests_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_device_auth_requests_device_code_hash": { + "name": "UQ_device_auth_requests_device_code_hash", + "columns": [ + { + "expression": "device_code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"device_auth_requests\".\"device_code_hash\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_device_auth_requests_user_code": { + "name": "UQ_device_auth_requests_user_code", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"device_auth_requests\".\"user_code\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_auth_requests_kilo_user_id_kilocode_users_id_fk": { + "name": "device_auth_requests_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "device_auth_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_refresh_tokens": { + "name": "device_refresh_tokens", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "device_session_id": { + "name": "device_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_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": { + "IDX_device_refresh_tokens_device_session_id": { + "name": "IDX_device_refresh_tokens_device_session_id", + "columns": [ + { + "expression": "device_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_refresh_tokens_expires_at": { + "name": "IDX_device_refresh_tokens_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_refresh_tokens_device_session_id_device_sessions_id_fk": { + "name": "device_refresh_tokens_device_session_id_device_sessions_id_fk", + "tableFrom": "device_refresh_tokens", + "tableTo": "device_sessions", + "columnsFrom": [ + "device_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_sessions": { + "name": "device_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_auth_request_id": { + "name": "device_auth_request_id", + "type": "uuid", + "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()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_reason": { + "name": "revoked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_device_sessions_kilo_user_id": { + "name": "IDX_device_sessions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_device_sessions_revoked_at": { + "name": "IDX_device_sessions_revoked_at", + "columns": [ + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "device_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "device_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.direct_byok_model_lists": { + "name": "direct_byok_model_lists", + "schema": "", + "columns": { + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "models": { + "name": "models", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord_gateway_listener": { + "name": "discord_gateway_listener", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "default": 1 + }, + "listener_id": { + "name": "listener_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.editor_name": { + "name": "editor_name", + "schema": "", + "columns": { + "editor_name_id": { + "name": "editor_name_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "editor_name": { + "name": "editor_name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_editor_name": { + "name": "UQ_editor_name", + "columns": [ + { + "expression": "editor_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.enkrypt_sync_state": { + "name": "enkrypt_sync_state", + "schema": "", + "columns": { + "job_name": { + "name": "job_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "attempt_id": { + "name": "attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_completed_at": { + "name": "last_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_outcome": { + "name": "last_outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_failure_category": { + "name": "last_failure_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_counts": { + "name": "last_counts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_success_counts": { + "name": "last_success_counts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified_models": { + "name": "verified_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "baseline_matched_count": { + "name": "baseline_matched_count", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "enkrypt_sync_state_singleton": { + "name": "enkrypt_sync_state_singleton", + "value": "\"enkrypt_sync_state\".\"job_name\" = 'enkrypt'" + }, + "enkrypt_sync_state_outcome": { + "name": "enkrypt_sync_state_outcome", + "value": "\"enkrypt_sync_state\".\"last_outcome\" IN ('running', 'succeeded', 'failed')" + }, + "enkrypt_sync_state_baseline": { + "name": "enkrypt_sync_state_baseline", + "value": "\"enkrypt_sync_state\".\"baseline_matched_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.enrichment_data": { + "name": "enrichment_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_enrichment_data": { + "name": "github_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "linkedin_enrichment_data": { + "name": "linkedin_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "clay_enrichment_data": { + "name": "clay_enrichment_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_enrichment_data_user_id": { + "name": "IDX_enrichment_data_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "enrichment_data_user_id_kilocode_users_id_fk": { + "name": "enrichment_data_user_id_kilocode_users_id_fk", + "tableFrom": "enrichment_data", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_enrichment_data_user_id": { + "name": "UQ_enrichment_data_user_id", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exa_monthly_usage": { + "name": "exa_monthly_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "month": { + "name": "month", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_charged_microdollars": { + "name": "total_charged_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "free_allowance_microdollars": { + "name": "free_allowance_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 10000000 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_exa_monthly_usage_personal": { + "name": "idx_exa_monthly_usage_personal", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"exa_monthly_usage\".\"organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_exa_monthly_usage_org": { + "name": "idx_exa_monthly_usage_org", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"exa_monthly_usage\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exa_usage_log": { + "name": "exa_usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "charged_to_balance": { + "name": "charged_to_balance", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_exa_usage_log_user_created": { + "name": "idx_exa_usage_log_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "exa_usage_log_id_created_at_pk": { + "name": "exa_usage_log_id_created_at_pk", + "columns": [ + "id", + "created_at" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.external_side_effect_outbox": { + "name": "external_side_effect_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'send_org_invite_email'" + }, + "invitation_id": { + "name": "invitation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_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()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_external_side_effect_outbox_invitation_id": { + "name": "UQ_external_side_effect_outbox_invitation_id", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_external_side_effect_outbox_status_next_attempt_at": { + "name": "IDX_external_side_effect_outbox_status_next_attempt_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feature": { + "name": "feature", + "schema": "", + "columns": { + "feature_id": { + "name": "feature_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_feature": { + "name": "UQ_feature", + "columns": [ + { + "expression": "feature", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finish_reason": { + "name": "finish_reason", + "schema": "", + "columns": { + "finish_reason_id": { + "name": "finish_reason_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "finish_reason": { + "name": "finish_reason", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_finish_reason": { + "name": "UQ_finish_reason", + "columns": [ + { + "expression": "finish_reason", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_app_installations": { + "name": "github_app_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "repository_access": { + "name": "repository_access", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repositories": { + "name": "repositories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "repositories_synced_at": { + "name": "repositories_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lifecycle_state": { + "name": "lifecycle_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_at": { + "name": "auth_invalid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_reason": { + "name": "auth_invalid_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sharing_mode": { + "name": "sharing_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'exclusive'" + }, + "sharing_admission_checked_at": { + "name": "sharing_admission_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observed_at": { + "name": "observed_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": { + "UQ_github_app_installations_app_installation": { + "name": "UQ_github_app_installations_app_installation", + "columns": [ + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_app_installations_app_type_check": { + "name": "github_app_installations_app_type_check", + "value": "\"github_app_installations\".\"github_app_type\" IN ('standard', 'lite')" + }, + "github_app_installations_installation_id_check": { + "name": "github_app_installations_installation_id_check", + "value": "\"github_app_installations\".\"installation_id\" ~ '^[1-9][0-9]*$'" + }, + "github_app_installations_lifecycle_state_check": { + "name": "github_app_installations_lifecycle_state_check", + "value": "\"github_app_installations\".\"lifecycle_state\" IN ('unknown', 'active', 'suspended', 'deleted')" + }, + "github_app_installations_sharing_mode_check": { + "name": "github_app_installations_sharing_mode_check", + "value": "\"github_app_installations\".\"sharing_mode\" IN ('exclusive', 'web_cloud_agent')" + } + }, + "isRLSEnabled": false + }, + "public.github_branch_pull_requests": { + "name": "github_branch_pull_requests", + "schema": "", + "columns": { + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_state": { + "name": "pr_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_title": { + "name": "pr_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_head_sha": { + "name": "pr_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_review_decision": { + "name": "pr_review_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_decision_pending": { + "name": "review_decision_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "review_decision_fetching_at": { + "name": "review_decision_fetching_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_last_synced_at": { + "name": "pr_last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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": { + "UQ_github_branch_prs_repo_pr_org": { + "name": "UQ_github_branch_prs_repo_pr_org", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_branch_pull_requests\".\"pr_number\" is not null", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_github_branch_prs_repo_pr_user": { + "name": "UQ_github_branch_prs_repo_pr_user", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"github_branch_pull_requests\".\"pr_number\" is not null", + "concurrently": true, + "method": "btree", + "with": {} + }, + "IDX_github_branch_prs_url_pr_number": { + "name": "IDX_github_branch_prs_url_pr_number", + "columns": [ + { + "expression": "git_url", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_branch_pull_requests_owned_by_organization_id_organizations_id_fk": { + "name": "github_branch_pull_requests_owned_by_organization_id_organizations_id_fk", + "tableFrom": "github_branch_pull_requests", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_branch_pull_requests_owned_by_user_id_kilocode_users_id_fk": { + "name": "github_branch_pull_requests_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "github_branch_pull_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_branch_pull_requests_owner_check": { + "name": "github_branch_pull_requests_owner_check", + "value": "(\n (\"github_branch_pull_requests\".\"owned_by_organization_id\" IS NOT NULL AND \"github_branch_pull_requests\".\"owned_by_user_id\" IS NULL) OR\n (\"github_branch_pull_requests\".\"owned_by_organization_id\" IS NULL AND \"github_branch_pull_requests\".\"owned_by_user_id\" IS NOT NULL)\n )" + }, + "github_branch_pull_requests_review_decision_check": { + "name": "github_branch_pull_requests_review_decision_check", + "value": "\"github_branch_pull_requests\".\"pr_review_decision\" IS NULL OR \"github_branch_pull_requests\".\"pr_review_decision\" IN ('approved', 'changes_requested', 'review_required')" + } + }, + "isRLSEnabled": false + }, + "public.github_connection_attempts": { + "name": "github_connection_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "return_to": { + "name": "return_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "selected_installation_id": { + "name": "selected_installation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_user_id": { + "name": "github_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "eligible_installations": { + "name": "eligible_installations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "completed_integration_id": { + "name": "completed_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_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": { + "IDX_github_connection_attempts_expires_at": { + "name": "IDX_github_connection_attempts_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_github_connection_attempts_completed_integration_id": { + "name": "IDX_github_connection_attempts_completed_integration_id", + "columns": [ + { + "expression": "completed_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_connection_attempts_completed_integration_id_platform_integrations_id_fk": { + "name": "github_connection_attempts_completed_integration_id_platform_integrations_id_fk", + "tableFrom": "github_connection_attempts", + "tableTo": "platform_integrations", + "columnsFrom": [ + "completed_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_connection_attempts_owner_type_check": { + "name": "github_connection_attempts_owner_type_check", + "value": "\"github_connection_attempts\".\"owner_type\" IN ('user', 'org')" + }, + "github_connection_attempts_app_type_check": { + "name": "github_connection_attempts_app_type_check", + "value": "\"github_connection_attempts\".\"github_app_type\" IN ('standard', 'lite')" + } + }, + "isRLSEnabled": false + }, + "public.github_install_states": { + "name": "github_install_states", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "return_to": { + "name": "return_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_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": { + "IDX_github_install_states_expires_at": { + "name": "IDX_github_install_states_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_install_states_kilo_user_id_kilocode_users_id_fk": { + "name": "github_install_states_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "github_install_states", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_install_states_owner_type_check": { + "name": "github_install_states_owner_type_check", + "value": "\"github_install_states\".\"owner_type\" IN ('org', 'user')" + } + }, + "isRLSEnabled": false + }, + "public.github_installation_webhook_receipts": { + "name": "github_installation_webhook_receipts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "github_installation_id": { + "name": "github_installation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'completed'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_github_installation_webhook_receipts_delivery": { + "name": "UQ_github_installation_webhook_receipts_delivery", + "columns": [ + { + "expression": "github_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_installation_webhook_receipts_github_installation_id_github_app_installations_id_fk": { + "name": "github_installation_webhook_receipts_github_installation_id_github_app_installations_id_fk", + "tableFrom": "github_installation_webhook_receipts", + "tableTo": "github_app_installations", + "columnsFrom": [ + "github_installation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_installation_webhook_receipts_status_check": { + "name": "github_installation_webhook_receipts_status_check", + "value": "\"github_installation_webhook_receipts\".\"status\" IN ('processing', 'completed')" + } + }, + "isRLSEnabled": false + }, + "public.http_ip": { + "name": "http_ip", + "schema": "", + "columns": { + "http_ip_id": { + "name": "http_ip_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "http_ip": { + "name": "http_ip", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_http_ip": { + "name": "UQ_http_ip", + "columns": [ + { + "expression": "http_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.http_user_agent": { + "name": "http_user_agent", + "schema": "", + "columns": { + "http_user_agent_id": { + "name": "http_user_agent_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_http_user_agent": { + "name": "UQ_http_user_agent", + "columns": [ + { + "expression": "http_user_agent", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.impact_advocate_participants": { + "name": "impact_advocate_participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "advocate_id": { + "name": "advocate_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "advocate_account_id": { + "name": "advocate_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_referral_identifier": { + "name": "opaque_referral_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_email": { + "name": "contact_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_state": { + "name": "registration_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_registration_attempt_at": { + "name": "last_registration_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_message": { + "name": "last_error_message", + "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": { + "UQ_impact_advocate_participants_program_referral_identifier": { + "name": "UQ_impact_advocate_participants_program_referral_identifier", + "columns": [ + { + "expression": "program_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "opaque_referral_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"impact_advocate_participants\".\"opaque_referral_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_participants_registration_state": { + "name": "IDX_impact_advocate_participants_registration_state", + "columns": [ + { + "expression": "registration_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_participants_user_id_kilocode_users_id_fk": { + "name": "impact_advocate_participants_user_id_kilocode_users_id_fk", + "tableFrom": "impact_advocate_participants", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_participants_program_user": { + "name": "UQ_impact_advocate_participants_program_user", + "nullsNotDistinct": false, + "columns": [ + "program_key", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_participants_program_key_check": { + "name": "impact_advocate_participants_program_key_check", + "value": "\"impact_advocate_participants\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_advocate_participants_registration_state_check": { + "name": "impact_advocate_participants_registration_state_check", + "value": "\"impact_advocate_participants\".\"registration_state\" IN ('pending', 'retrying', 'registered', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.impact_advocate_registration_attempts": { + "name": "impact_advocate_registration_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_cookie_value": { + "name": "opaque_cookie_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cookie_value_length": { + "name": "cookie_value_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_payload": { + "name": "response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_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": { + "IDX_impact_advocate_registration_attempts_participant_id": { + "name": "IDX_impact_advocate_registration_attempts_participant_id", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_registration_attempts_delivery_state": { + "name": "IDX_impact_advocate_registration_attempts_delivery_state", + "columns": [ + { + "expression": "delivery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_registration_attempts_participant_id_impact_advocate_participants_id_fk": { + "name": "impact_advocate_registration_attempts_participant_id_impact_advocate_participants_id_fk", + "tableFrom": "impact_advocate_registration_attempts", + "tableTo": "impact_advocate_participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_registration_attempts_dedupe_key": { + "name": "UQ_impact_advocate_registration_attempts_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_registration_attempts_program_key_check": { + "name": "impact_advocate_registration_attempts_program_key_check", + "value": "\"impact_advocate_registration_attempts\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_advocate_registration_attempts_delivery_state_check": { + "name": "impact_advocate_registration_attempts_delivery_state_check", + "value": "\"impact_advocate_registration_attempts\".\"delivery_state\" IN ('queued', 'sending', 'succeeded', 'failed')" + }, + "impact_advocate_registration_attempts_cookie_value_length_non_negative_check": { + "name": "impact_advocate_registration_attempts_cookie_value_length_non_negative_check", + "value": "\"impact_advocate_registration_attempts\".\"cookie_value_length\" >= 0" + }, + "impact_advocate_registration_attempts_attempt_count_non_negative_check": { + "name": "impact_advocate_registration_attempts_attempt_count_non_negative_check", + "value": "\"impact_advocate_registration_attempts\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_advocate_reward_redemptions": { + "name": "impact_advocate_reward_redemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "reward_id": { + "name": "reward_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "impact_reward_id": { + "name": "impact_reward_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lookup_response_payload": { + "name": "lookup_response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "redeem_response_payload": { + "name": "redeem_response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "redeemed_at": { + "name": "redeemed_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": { + "IDX_impact_advocate_reward_redemptions_beneficiary_user_id": { + "name": "IDX_impact_advocate_reward_redemptions_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_advocate_reward_redemptions_state": { + "name": "IDX_impact_advocate_reward_redemptions_state", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_advocate_reward_redemptions_reward_id_impact_referral_rewards_id_fk": { + "name": "impact_advocate_reward_redemptions_reward_id_impact_referral_rewards_id_fk", + "tableFrom": "impact_advocate_reward_redemptions", + "tableTo": "impact_referral_rewards", + "columnsFrom": [ + "reward_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_advocate_reward_redemptions_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_advocate_reward_redemptions_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_advocate_reward_redemptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_advocate_reward_redemptions_reward_id": { + "name": "UQ_impact_advocate_reward_redemptions_reward_id", + "nullsNotDistinct": false, + "columns": [ + "reward_id" + ] + }, + "UQ_impact_advocate_reward_redemptions_dedupe_key": { + "name": "UQ_impact_advocate_reward_redemptions_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_advocate_reward_redemptions_state_check": { + "name": "impact_advocate_reward_redemptions_state_check", + "value": "\"impact_advocate_reward_redemptions\".\"state\" IN ('queued', 'retrying', 'redeemed', 'failed')" + }, + "impact_advocate_reward_redemptions_attempt_count_non_negative_check": { + "name": "impact_advocate_reward_redemptions_attempt_count_non_negative_check", + "value": "\"impact_advocate_reward_redemptions\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_attribution_touches": { + "name": "impact_attribution_touches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "program_key": { + "name": "program_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'kiloclaw'" + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anonymous_id": { + "name": "anonymous_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "touch_type": { + "name": "touch_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opaque_tracking_value": { + "name": "opaque_tracking_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tracking_value_length": { + "name": "tracking_value_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_tracking_value_accepted": { + "name": "is_tracking_value_accepted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "rs_code": { + "name": "rs_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rs_share_medium": { + "name": "rs_share_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rs_engagement_medium": { + "name": "rs_engagement_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "im_ref": { + "name": "im_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landing_path": { + "name": "landing_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_source": { + "name": "utm_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_medium": { + "name": "utm_medium", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_campaign": { + "name": "utm_campaign", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_term": { + "name": "utm_term", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "utm_content": { + "name": "utm_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "touched_at": { + "name": "touched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "sale_attributed_at": { + "name": "sale_attributed_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": { + "IDX_impact_attribution_touches_product_user_id": { + "name": "IDX_impact_attribution_touches_product_user_id", + "columns": [ + { + "expression": "product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_user_id": { + "name": "IDX_impact_attribution_touches_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_anonymous_id": { + "name": "IDX_impact_attribution_touches_anonymous_id", + "columns": [ + { + "expression": "anonymous_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_expires_at": { + "name": "IDX_impact_attribution_touches_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_attribution_touches_sale_attributed_at": { + "name": "IDX_impact_attribution_touches_sale_attributed_at", + "columns": [ + { + "expression": "sale_attributed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_attribution_touches_user_id_kilocode_users_id_fk": { + "name": "impact_attribution_touches_user_id_kilocode_users_id_fk", + "tableFrom": "impact_attribution_touches", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_attribution_touches_dedupe_key": { + "name": "UQ_impact_attribution_touches_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_attribution_touches_product_check": { + "name": "impact_attribution_touches_product_check", + "value": "\"impact_attribution_touches\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_attribution_touches_program_key_check": { + "name": "impact_attribution_touches_program_key_check", + "value": "\"impact_attribution_touches\".\"program_key\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_attribution_touches_touch_type_check": { + "name": "impact_attribution_touches_touch_type_check", + "value": "\"impact_attribution_touches\".\"touch_type\" IN ('affiliate', 'referral')" + }, + "impact_attribution_touches_provider_check": { + "name": "impact_attribution_touches_provider_check", + "value": "\"impact_attribution_touches\".\"provider\" IN ('impact_performance', 'impact_advocate')" + }, + "impact_attribution_touches_tracking_value_length_non_negative_check": { + "name": "impact_attribution_touches_tracking_value_length_non_negative_check", + "value": "\"impact_attribution_touches\".\"tracking_value_length\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_conversion_reports": { + "name": "impact_conversion_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action_tracker_id": { + "name": "action_tracker_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "order_id": { + "name": "order_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_payload": { + "name": "response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_status_code": { + "name": "response_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "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": { + "IDX_impact_conversion_reports_conversion_id": { + "name": "IDX_impact_conversion_reports_conversion_id", + "columns": [ + { + "expression": "conversion_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_conversion_reports_state": { + "name": "IDX_impact_conversion_reports_state", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_conversion_reports_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_conversion_reports_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_conversion_reports", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_conversion_reports_dedupe_key": { + "name": "UQ_impact_conversion_reports_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_conversion_reports_state_check": { + "name": "impact_conversion_reports_state_check", + "value": "\"impact_conversion_reports\".\"state\" IN ('queued', 'retrying', 'delivered', 'failed')" + }, + "impact_conversion_reports_attempt_count_non_negative_check": { + "name": "impact_conversion_reports_attempt_count_non_negative_check", + "value": "\"impact_conversion_reports\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_conversions": { + "name": "impact_referral_conversions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "referee_user_id": { + "name": "referee_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer_user_id": { + "name": "referrer_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_touch_id": { + "name": "source_touch_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "winning_touch_type": { + "name": "winning_touch_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'credits'" + }, + "source_payment_id": { + "name": "source_payment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "qualified": { + "name": "qualified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disqualification_reason": { + "name": "disqualification_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "converted_at": { + "name": "converted_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": { + "IDX_impact_referral_conversions_referee_user_id": { + "name": "IDX_impact_referral_conversions_referee_user_id", + "columns": [ + { + "expression": "referee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_conversions_referrer_user_id": { + "name": "IDX_impact_referral_conversions_referrer_user_id", + "columns": [ + { + "expression": "referrer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_conversions_referee_user_id_kilocode_users_id_fk": { + "name": "impact_referral_conversions_referee_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_conversions_referrer_user_id_kilocode_users_id_fk": { + "name": "impact_referral_conversions_referrer_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referrer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "impact_referral_conversions_source_touch_id_impact_attribution_touches_id_fk": { + "name": "impact_referral_conversions_source_touch_id_impact_attribution_touches_id_fk", + "tableFrom": "impact_referral_conversions", + "tableTo": "impact_attribution_touches", + "columnsFrom": [ + "source_touch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_conversions_product_payment_source": { + "name": "UQ_impact_referral_conversions_product_payment_source", + "nullsNotDistinct": false, + "columns": [ + "product", + "payment_provider", + "source_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_conversions_product_check": { + "name": "impact_referral_conversions_product_check", + "value": "\"impact_referral_conversions\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_conversions_winning_touch_type_check": { + "name": "impact_referral_conversions_winning_touch_type_check", + "value": "\"impact_referral_conversions\".\"winning_touch_type\" IN ('referral', 'affiliate', 'none')" + }, + "impact_referral_conversions_payment_provider_check": { + "name": "impact_referral_conversions_payment_provider_check", + "value": "\"impact_referral_conversions\".\"payment_provider\" IN ('stripe', 'credits', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_reward_applications": { + "name": "impact_referral_reward_applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "reward_id": { + "name": "reward_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "previous_renewal_boundary": { + "name": "previous_renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "new_renewal_boundary": { + "name": "new_renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "local_operation_id": { + "name": "local_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_operation_id": { + "name": "stripe_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_idempotency_key": { + "name": "stripe_idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applied_at": { + "name": "applied_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": { + "IDX_impact_referral_reward_applications_reward_id": { + "name": "IDX_impact_referral_reward_applications_reward_id", + "columns": [ + { + "expression": "reward_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_reward_applications_beneficiary_user_id": { + "name": "IDX_impact_referral_reward_applications_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_reward_applications_reward_id_impact_referral_rewards_id_fk": { + "name": "impact_referral_reward_applications_reward_id_impact_referral_rewards_id_fk", + "tableFrom": "impact_referral_reward_applications", + "tableTo": "impact_referral_rewards", + "columnsFrom": [ + "reward_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_reward_applications_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_reward_applications_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_reward_applications", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "impact_referral_reward_applications_product_check": { + "name": "impact_referral_reward_applications_product_check", + "value": "\"impact_referral_reward_applications\".\"product\" IN ('kiloclaw', 'kilo_pass')" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_reward_decisions": { + "name": "impact_referral_reward_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_role": { + "name": "beneficiary_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_kind": { + "name": "reward_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw_free_month'" + }, + "months_granted": { + "name": "months_granted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reward_percent": { + "name": "reward_percent", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "source_tier": { + "name": "source_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount_usd": { + "name": "reward_amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_reward_decisions_beneficiary_user_id": { + "name": "IDX_impact_referral_reward_decisions_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_reward_decisions_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_referral_reward_decisions_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_referral_reward_decisions", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_reward_decisions_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_reward_decisions_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_reward_decisions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_reward_decisions_conversion_role": { + "name": "UQ_impact_referral_reward_decisions_conversion_role", + "nullsNotDistinct": false, + "columns": [ + "conversion_id", + "beneficiary_role" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_reward_decisions_product_check": { + "name": "impact_referral_reward_decisions_product_check", + "value": "\"impact_referral_reward_decisions\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_reward_decisions_beneficiary_role_check": { + "name": "impact_referral_reward_decisions_beneficiary_role_check", + "value": "\"impact_referral_reward_decisions\".\"beneficiary_role\" IN ('referrer', 'referee')" + }, + "impact_referral_reward_decisions_outcome_check": { + "name": "impact_referral_reward_decisions_outcome_check", + "value": "\"impact_referral_reward_decisions\".\"outcome\" IN ('granted', 'cap_limited', 'disqualified')" + }, + "impact_referral_reward_decisions_reward_kind_check": { + "name": "impact_referral_reward_decisions_reward_kind_check", + "value": "\"impact_referral_reward_decisions\".\"reward_kind\" IN ('kiloclaw_free_month', 'kilo_pass_bonus')" + }, + "impact_referral_reward_decisions_months_granted_non_negative_check": { + "name": "impact_referral_reward_decisions_months_granted_non_negative_check", + "value": "\"impact_referral_reward_decisions\".\"months_granted\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referral_rewards": { + "name": "impact_referral_rewards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "conversion_id": { + "name": "conversion_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "decision_id": { + "name": "decision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "beneficiary_user_id": { + "name": "beneficiary_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "beneficiary_role": { + "name": "beneficiary_role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reward_kind": { + "name": "reward_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw_free_month'" + }, + "months_granted": { + "name": "months_granted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "reward_percent": { + "name": "reward_percent", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "source_tier": { + "name": "source_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_amount_usd": { + "name": "reward_amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "applies_to_subscription_id": { + "name": "applies_to_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applies_to_kilo_pass_subscription_id": { + "name": "applies_to_kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "consumed_kilo_pass_issuance_id": { + "name": "consumed_kilo_pass_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "consumed_kilo_pass_issuance_item_id": { + "name": "consumed_kilo_pass_issuance_item_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "earned_at": { + "name": "earned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reversed_at": { + "name": "reversed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_reason": { + "name": "review_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referral_rewards_beneficiary_user_id": { + "name": "IDX_impact_referral_rewards_beneficiary_user_id", + "columns": [ + { + "expression": "beneficiary_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referral_rewards_status": { + "name": "IDX_impact_referral_rewards_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referral_rewards_conversion_id_impact_referral_conversions_id_fk": { + "name": "impact_referral_rewards_conversion_id_impact_referral_conversions_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "impact_referral_conversions", + "columnsFrom": [ + "conversion_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_rewards_decision_id_impact_referral_reward_decisions_id_fk": { + "name": "impact_referral_rewards_decision_id_impact_referral_reward_decisions_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "impact_referral_reward_decisions", + "columnsFrom": [ + "decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referral_rewards_beneficiary_user_id_kilocode_users_id_fk": { + "name": "impact_referral_rewards_beneficiary_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilocode_users", + "columnsFrom": [ + "beneficiary_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_subscription": { + "name": "FK_impact_referral_rewards_kilo_pass_subscription", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "applies_to_kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_issuance": { + "name": "FK_impact_referral_rewards_kilo_pass_issuance", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "consumed_kilo_pass_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "FK_impact_referral_rewards_kilo_pass_issuance_item": { + "name": "FK_impact_referral_rewards_kilo_pass_issuance_item", + "tableFrom": "impact_referral_rewards", + "tableTo": "kilo_pass_issuance_items", + "columnsFrom": [ + "consumed_kilo_pass_issuance_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referral_rewards_conversion_role": { + "name": "UQ_impact_referral_rewards_conversion_role", + "nullsNotDistinct": false, + "columns": [ + "conversion_id", + "beneficiary_role" + ] + }, + "UQ_impact_referral_rewards_decision_id": { + "name": "UQ_impact_referral_rewards_decision_id", + "nullsNotDistinct": false, + "columns": [ + "decision_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referral_rewards_product_check": { + "name": "impact_referral_rewards_product_check", + "value": "\"impact_referral_rewards\".\"product\" IN ('kiloclaw', 'kilo_pass')" + }, + "impact_referral_rewards_beneficiary_role_check": { + "name": "impact_referral_rewards_beneficiary_role_check", + "value": "\"impact_referral_rewards\".\"beneficiary_role\" IN ('referrer', 'referee')" + }, + "impact_referral_rewards_reward_kind_check": { + "name": "impact_referral_rewards_reward_kind_check", + "value": "\"impact_referral_rewards\".\"reward_kind\" IN ('kiloclaw_free_month', 'kilo_pass_bonus')" + }, + "impact_referral_rewards_status_check": { + "name": "impact_referral_rewards_status_check", + "value": "\"impact_referral_rewards\".\"status\" IN ('pending', 'earned', 'applied', 'reversed', 'expired', 'canceled', 'review_required')" + }, + "impact_referral_rewards_months_granted_non_negative_check": { + "name": "impact_referral_rewards_months_granted_non_negative_check", + "value": "\"impact_referral_rewards\".\"months_granted\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.impact_referrals": { + "name": "impact_referrals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kiloclaw'" + }, + "referee_user_id": { + "name": "referee_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer_user_id": { + "name": "referrer_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_touch_id": { + "name": "source_touch_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "impact_referral_id": { + "name": "impact_referral_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_impact_referrals_referrer_user_id": { + "name": "IDX_impact_referrals_referrer_user_id", + "columns": [ + { + "expression": "referrer_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_impact_referrals_source_touch_id": { + "name": "IDX_impact_referrals_source_touch_id", + "columns": [ + { + "expression": "source_touch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "impact_referrals_referee_user_id_kilocode_users_id_fk": { + "name": "impact_referrals_referee_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "impact_referrals_referrer_user_id_kilocode_users_id_fk": { + "name": "impact_referrals_referrer_user_id_kilocode_users_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "referrer_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "impact_referrals_source_touch_id_impact_attribution_touches_id_fk": { + "name": "impact_referrals_source_touch_id_impact_attribution_touches_id_fk", + "tableFrom": "impact_referrals", + "tableTo": "impact_attribution_touches", + "columnsFrom": [ + "source_touch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_impact_referrals_product_referee_user_id": { + "name": "UQ_impact_referrals_product_referee_user_id", + "nullsNotDistinct": false, + "columns": [ + "product", + "referee_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "impact_referrals_product_check": { + "name": "impact_referrals_product_check", + "value": "\"impact_referrals\".\"product\" IN ('kiloclaw', 'kilo_pass')" + } + }, + "isRLSEnabled": false + }, + "public.ja4_digest": { + "name": "ja4_digest", + "schema": "", + "columns": { + "ja4_digest_id": { + "name": "ja4_digest_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "ja4_digest": { + "name": "ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_ja4_digest": { + "name": "UQ_ja4_digest", + "columns": [ + { + "expression": "ja4_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilo_pass_audit_log": { + "name": "kilo_pass_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "related_credit_transaction_id": { + "name": "related_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "related_monthly_issuance_id": { + "name": "related_monthly_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "IDX_kilo_pass_audit_log_created_at": { + "name": "IDX_kilo_pass_audit_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_kilo_user_id": { + "name": "IDX_kilo_pass_audit_log_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_kilo_pass_subscription_id": { + "name": "IDX_kilo_pass_audit_log_kilo_pass_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_action": { + "name": "IDX_kilo_pass_audit_log_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_result": { + "name": "IDX_kilo_pass_audit_log_result", + "columns": [ + { + "expression": "result", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_idempotency_key": { + "name": "IDX_kilo_pass_audit_log_idempotency_key", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_event_id": { + "name": "IDX_kilo_pass_audit_log_stripe_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_invoice_id": { + "name": "IDX_kilo_pass_audit_log_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_stripe_subscription_id": { + "name": "IDX_kilo_pass_audit_log_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_related_credit_transaction_id": { + "name": "IDX_kilo_pass_audit_log_related_credit_transaction_id", + "columns": [ + { + "expression": "related_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_audit_log_related_monthly_issuance_id": { + "name": "IDX_kilo_pass_audit_log_related_monthly_issuance_id", + "columns": [ + { + "expression": "related_monthly_issuance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_audit_log_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_audit_log_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_audit_log_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_related_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_audit_log_related_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "credit_transactions", + "columnsFrom": [ + "related_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_audit_log_related_monthly_issuance_id_kilo_pass_issuances_id_fk": { + "name": "kilo_pass_audit_log_related_monthly_issuance_id_kilo_pass_issuances_id_fk", + "tableFrom": "kilo_pass_audit_log", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "related_monthly_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_audit_log_action_check": { + "name": "kilo_pass_audit_log_action_check", + "value": "\"kilo_pass_audit_log\".\"action\" IN ('stripe_webhook_received', 'kilo_pass_invoice_paid_handled', 'store_purchase_completed', 'store_notification_received', 'store_subscription_renewed', 'store_subscription_canceled', 'store_subscription_expired', 'store_subscription_refunded', 'base_credits_issued', 'bonus_credits_issued', 'bonus_credits_skipped_idempotent', 'first_month_50pct_promo_issued', 'yearly_monthly_base_cron_started', 'yearly_monthly_base_cron_completed', 'issue_yearly_remaining_credits', 'duplicate_card_subscription_canceled', 'yearly_monthly_bonus_cron_started', 'yearly_monthly_bonus_cron_completed')" + }, + "kilo_pass_audit_log_result_check": { + "name": "kilo_pass_audit_log_result_check", + "value": "\"kilo_pass_audit_log\".\"result\" IN ('success', 'skipped_idempotent', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_issuance_items": { + "name": "kilo_pass_issuance_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_issuance_id": { + "name": "kilo_pass_issuance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "bonus_percent_applied": { + "name": "bonus_percent_applied", + "type": "numeric(6, 4)", + "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": { + "IDX_kilo_pass_issuance_items_issuance_id": { + "name": "IDX_kilo_pass_issuance_items_issuance_id", + "columns": [ + { + "expression": "kilo_pass_issuance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuance_items_credit_transaction_id": { + "name": "IDX_kilo_pass_issuance_items_credit_transaction_id", + "columns": [ + { + "expression": "credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_issuance_items_kilo_pass_issuance_id_kilo_pass_issuances_id_fk": { + "name": "kilo_pass_issuance_items_kilo_pass_issuance_id_kilo_pass_issuances_id_fk", + "tableFrom": "kilo_pass_issuance_items", + "tableTo": "kilo_pass_issuances", + "columnsFrom": [ + "kilo_pass_issuance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_issuance_items_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_issuance_items_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_issuance_items", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilo_pass_issuance_items_credit_transaction_id_unique": { + "name": "kilo_pass_issuance_items_credit_transaction_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credit_transaction_id" + ] + }, + "UQ_kilo_pass_issuance_items_issuance_kind": { + "name": "UQ_kilo_pass_issuance_items_issuance_kind", + "nullsNotDistinct": false, + "columns": [ + "kilo_pass_issuance_id", + "kind" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_issuance_items_bonus_percent_applied_range_check": { + "name": "kilo_pass_issuance_items_bonus_percent_applied_range_check", + "value": "\"kilo_pass_issuance_items\".\"bonus_percent_applied\" IS NULL OR (\"kilo_pass_issuance_items\".\"bonus_percent_applied\" >= 0 AND \"kilo_pass_issuance_items\".\"bonus_percent_applied\" <= 1)" + }, + "kilo_pass_issuance_items_amount_usd_non_negative_check": { + "name": "kilo_pass_issuance_items_amount_usd_non_negative_check", + "value": "\"kilo_pass_issuance_items\".\"amount_usd\" >= 0" + }, + "kilo_pass_issuance_items_kind_check": { + "name": "kilo_pass_issuance_items_kind_check", + "value": "\"kilo_pass_issuance_items\".\"kind\" IN ('base', 'bonus', 'promo_first_month_50pct', 'referral_bonus')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_issuances": { + "name": "kilo_pass_issuances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_month": { + "name": "issue_month", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initial_welcome_promo_eligibility_reason": { + "name": "initial_welcome_promo_eligibility_reason", + "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": { + "UQ_kilo_pass_issuances_stripe_invoice_id": { + "name": "UQ_kilo_pass_issuances_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_issuances\".\"stripe_invoice_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuances_subscription_id": { + "name": "IDX_kilo_pass_issuances_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_issuances_issue_month": { + "name": "IDX_kilo_pass_issuances_issue_month", + "columns": [ + { + "expression": "issue_month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_issuances_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_issuances_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_issuances", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_issuances_subscription_issue_month": { + "name": "UQ_kilo_pass_issuances_subscription_issue_month", + "nullsNotDistinct": false, + "columns": [ + "kilo_pass_subscription_id", + "issue_month" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_issuances_issue_month_day_one_check": { + "name": "kilo_pass_issuances_issue_month_day_one_check", + "value": "EXTRACT(DAY FROM \"kilo_pass_issuances\".\"issue_month\") = 1" + }, + "kilo_pass_issuances_source_check": { + "name": "kilo_pass_issuances_source_check", + "value": "\"kilo_pass_issuances\".\"source\" IN ('stripe_invoice', 'app_store_transaction', 'google_play_transaction', 'cron')" + }, + "kilo_pass_issuances_initial_welcome_promo_reason_check": { + "name": "kilo_pass_issuances_initial_welcome_promo_reason_check", + "value": "\"kilo_pass_issuances\".\"initial_welcome_promo_eligibility_reason\" IN ('first_payment_fingerprint_claim', 'fingerprint_previously_claimed', 'missing_fingerprint', 'no_supported_fingerprint', 'no_positive_settlement', 'settlement_unresolved')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_agreements": { + "name": "kilo_pass_org_agreements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "parent_organization_id": { + "name": "parent_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "term_version_id": { + "name": "term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processing_condition": { + "name": "processing_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "purchase_channel": { + "name": "purchase_channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchased_pass_capacity": { + "name": "purchased_pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "next_purchased_pass_capacity": { + "name": "next_purchased_pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_capacity_effective_at": { + "name": "next_capacity_effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_from": { + "name": "paid_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_until": { + "name": "paid_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issuance_anchor_at": { + "name": "issuance_anchor_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_seat_add_on_item_id": { + "name": "provider_seat_add_on_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activation_provider_event_id": { + "name": "activation_provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_contract_id": { + "name": "external_contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_review_required_at": { + "name": "payment_review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_effective_at": { + "name": "cancellation_effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "manually_issued_through": { + "name": "manually_issued_through", + "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": { + "UQ_kilo_pass_org_agreements_one_non_ended_parent": { + "name": "UQ_kilo_pass_org_agreements_one_non_ended_parent", + "columns": [ + { + "expression": "parent_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"state\" <> 'ended'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_provider_subscription": { + "name": "UQ_kilo_pass_org_agreements_provider_subscription", + "columns": [ + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"provider_subscription_id\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"state\" <> 'ended'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_provider_seat_add_on_item": { + "name": "UQ_kilo_pass_org_agreements_provider_seat_add_on_item", + "columns": [ + { + "expression": "provider_seat_add_on_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"provider_seat_add_on_item_id\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"state\" <> 'ended'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_external_contract": { + "name": "UQ_kilo_pass_org_agreements_external_contract", + "columns": [ + { + "expression": "external_contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"external_contract_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_agreements_activation_provider_event": { + "name": "UQ_kilo_pass_org_agreements_activation_provider_event", + "columns": [ + { + "expression": "activation_provider_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_agreements\".\"activation_provider_event_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_org_agreements_processing": { + "name": "IDX_kilo_pass_org_agreements_processing", + "columns": [ + { + "expression": "processing_condition", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_agreements_parent_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_agreements_parent_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_agreements", + "tableTo": "organizations", + "columnsFrom": [ + "parent_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_agreements_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_agreements_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_agreements", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_agreements_purchased_capacity_non_negative_check": { + "name": "kilo_pass_org_agreements_purchased_capacity_non_negative_check", + "value": "\"kilo_pass_org_agreements\".\"purchased_pass_capacity\" >= 0" + }, + "kilo_pass_org_agreements_next_capacity_check": { + "name": "kilo_pass_org_agreements_next_capacity_check", + "value": "(\"kilo_pass_org_agreements\".\"next_purchased_pass_capacity\" IS NULL AND \"kilo_pass_org_agreements\".\"next_capacity_effective_at\" IS NULL) OR (\"kilo_pass_org_agreements\".\"next_purchased_pass_capacity\" >= 0 AND \"kilo_pass_org_agreements\".\"next_capacity_effective_at\" IS NOT NULL)" + }, + "kilo_pass_org_agreements_paid_interval_check": { + "name": "kilo_pass_org_agreements_paid_interval_check", + "value": "(\"kilo_pass_org_agreements\".\"paid_from\" IS NULL AND \"kilo_pass_org_agreements\".\"paid_until\" IS NULL) OR (\"kilo_pass_org_agreements\".\"paid_from\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"paid_until\" IS NOT NULL AND \"kilo_pass_org_agreements\".\"paid_from\" < \"kilo_pass_org_agreements\".\"paid_until\")" + }, + "kilo_pass_org_agreements_state_check": { + "name": "kilo_pass_org_agreements_state_check", + "value": "\"kilo_pass_org_agreements\".\"state\" IN ('pending_payment', 'active', 'cancel_at_period_end', 'ended')" + }, + "kilo_pass_org_agreements_processing_condition_check": { + "name": "kilo_pass_org_agreements_processing_condition_check", + "value": "\"kilo_pass_org_agreements\".\"processing_condition\" IN ('ready', 'manual', 'blocked', 'overallocated', 'failed', 'suspended_for_review')" + }, + "kilo_pass_org_agreements_purchase_channel_check": { + "name": "kilo_pass_org_agreements_purchase_channel_check", + "value": "\"kilo_pass_org_agreements\".\"purchase_channel\" IN ('self_serve', 'manual')" + }, + "kilo_pass_org_agreements_cadence_check": { + "name": "kilo_pass_org_agreements_cadence_check", + "value": "\"kilo_pass_org_agreements\".\"cadence\" IN ('monthly', 'yearly')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_allocation_plan_rows": { + "name": "kilo_pass_org_allocation_plan_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "allocation_plan_id": { + "name": "allocation_plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_container_organization_id": { + "name": "allocation_container_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pass_capacity": { + "name": "pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kilo_pass_org_allocation_plan_rows_positive_container": { + "name": "IDX_kilo_pass_org_allocation_plan_rows_positive_container", + "columns": [ + { + "expression": "allocation_container_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kilo_pass_org_allocation_plan_rows\".\"pass_capacity\" > 0", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_allocation_plan_rows_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk": { + "name": "kilo_pass_org_allocation_plan_rows_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk", + "tableFrom": "kilo_pass_org_allocation_plan_rows", + "tableTo": "kilo_pass_org_allocation_plans", + "columnsFrom": [ + "allocation_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_allocation_plan_rows_allocation_container_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_allocation_plan_rows_allocation_container_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_allocation_plan_rows", + "tableTo": "organizations", + "columnsFrom": [ + "allocation_container_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_allocation_plan_rows_plan_container": { + "name": "UQ_kilo_pass_org_allocation_plan_rows_plan_container", + "nullsNotDistinct": false, + "columns": [ + "allocation_plan_id", + "allocation_container_organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_allocation_plan_rows_capacity_non_negative_check": { + "name": "kilo_pass_org_allocation_plan_rows_capacity_non_negative_check", + "value": "\"kilo_pass_org_allocation_plan_rows\".\"pass_capacity\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_allocation_plans": { + "name": "kilo_pass_org_allocation_plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effective_window_start": { + "name": "effective_window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_allocation_plans_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_allocation_plans_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_allocation_plans", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_allocation_plans_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_allocation_plans_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_allocation_plans", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_allocation_plans_agreement_window": { + "name": "UQ_kilo_pass_org_allocation_plans_agreement_window", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "effective_window_start" + ] + }, + "UQ_kilo_pass_org_allocation_plans_agreement_version": { + "name": "UQ_kilo_pass_org_allocation_plans_agreement_version", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "version" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_allocation_plans_version_positive_check": { + "name": "kilo_pass_org_allocation_plans_version_positive_check", + "value": "\"kilo_pass_org_allocation_plans\".\"version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_audit_records": { + "name": "kilo_pass_org_audit_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_kilo_user_id": { + "name": "actor_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "before_json": { + "name": "before_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_json": { + "name": "after_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_org_audit_records_idempotency": { + "name": "UQ_kilo_pass_org_audit_records_idempotency", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_audit_records\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_org_audit_records_agreement_created": { + "name": "IDX_kilo_pass_org_audit_records_agreement_created", + "columns": [ + { + "expression": "agreement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_audit_records_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_audit_records_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_audit_records", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_org_audit_records_actor_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_audit_records_actor_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_audit_records", + "tableTo": "kilocode_users", + "columnsFrom": [ + "actor_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilo_pass_org_issuance_snapshots": { + "name": "kilo_pass_org_issuance_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "processing_run_id": { + "name": "processing_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "allocation_plan_id": { + "name": "allocation_plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "term_version_id": { + "name": "term_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_container_organization_id": { + "name": "allocation_container_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "qualifying_spend_starts_at": { + "name": "qualifying_spend_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tranche_key": { + "name": "tranche_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allocated_pass_capacity": { + "name": "allocated_pass_capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "base_credit_microdollars": { + "name": "base_credit_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bonus_credit_microdollars": { + "name": "bonus_credit_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "unlock_spend_microdollars": { + "name": "unlock_spend_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "qualifying_spend_microdollars": { + "name": "qualifying_spend_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bonus_mode": { + "name": "bonus_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bonus_unlocked_at": { + "name": "bonus_unlocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "repair_completed_at": { + "name": "repair_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "bonus_credit_transaction_id": { + "name": "bonus_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "base_credit_transaction_id": { + "name": "base_credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kilo_pass_org_issuance_snapshots_base_credit_transaction": { + "name": "UQ_kilo_pass_org_issuance_snapshots_base_credit_transaction", + "columns": [ + { + "expression": "base_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_issuance_snapshots\".\"base_credit_transaction_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_org_issuance_snapshots_bonus_credit_transaction": { + "name": "UQ_kilo_pass_org_issuance_snapshots_bonus_credit_transaction", + "columns": [ + { + "expression": "bonus_credit_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_org_issuance_snapshots\".\"bonus_credit_transaction_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_org_issuance_snapshots_window": { + "name": "IDX_kilo_pass_org_issuance_snapshots_window", + "columns": [ + { + "expression": "agreement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_issuance_snapshots_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_processing_run_id_kilo_pass_org_processing_runs_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_processing_run_id_kilo_pass_org_processing_runs_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_processing_runs", + "columnsFrom": [ + "processing_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_allocation_plan_id_kilo_pass_org_allocation_plans_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_allocation_plans", + "columnsFrom": [ + "allocation_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_term_version_id_kilo_pass_org_term_versions_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_term_version_id_kilo_pass_org_term_versions_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "kilo_pass_org_term_versions", + "columnsFrom": [ + "term_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_allocation_container_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_allocation_container_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "organizations", + "columnsFrom": [ + "allocation_container_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_bonus_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_bonus_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "credit_transactions", + "columnsFrom": [ + "bonus_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_issuance_snapshots_base_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_org_issuance_snapshots_base_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_org_issuance_snapshots", + "tableTo": "credit_transactions", + "columnsFrom": [ + "base_credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_issuance_snapshots_container_window_tranche": { + "name": "UQ_kilo_pass_org_issuance_snapshots_container_window_tranche", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "allocation_container_organization_id", + "window_start", + "tranche_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_issuance_snapshots_window_check": { + "name": "kilo_pass_org_issuance_snapshots_window_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"window_start\" < \"kilo_pass_org_issuance_snapshots\".\"window_end\"" + }, + "kilo_pass_org_issuance_snapshots_qualifying_spend_window_check": { + "name": "kilo_pass_org_issuance_snapshots_qualifying_spend_window_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"window_start\" <= \"kilo_pass_org_issuance_snapshots\".\"qualifying_spend_starts_at\" AND \"kilo_pass_org_issuance_snapshots\".\"qualifying_spend_starts_at\" < \"kilo_pass_org_issuance_snapshots\".\"window_end\"" + }, + "kilo_pass_org_issuance_snapshots_values_non_negative_check": { + "name": "kilo_pass_org_issuance_snapshots_values_non_negative_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"allocated_pass_capacity\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"base_credit_microdollars\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"bonus_credit_microdollars\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"unlock_spend_microdollars\" >= 0 AND \"kilo_pass_org_issuance_snapshots\".\"qualifying_spend_microdollars\" >= 0" + }, + "kilo_pass_org_issuance_snapshots_kind_check": { + "name": "kilo_pass_org_issuance_snapshots_kind_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"kind\" IN ('regular', 'bridge', 'supplement')" + }, + "kilo_pass_org_issuance_snapshots_bonus_mode_check": { + "name": "kilo_pass_org_issuance_snapshots_bonus_mode_check", + "value": "\"kilo_pass_org_issuance_snapshots\".\"bonus_mode\" IN ('after_base', 'upfront')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_notification_deliveries": { + "name": "kilo_pass_org_notification_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "processing_run_id": { + "name": "processing_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_kilo_user_id": { + "name": "recipient_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_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": { + "IDX_kilo_pass_org_notification_deliveries_status": { + "name": "IDX_kilo_pass_org_notification_deliveries_status", + "columns": [ + { + "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": { + "kilo_pass_org_notification_deliveries_processing_run_id_kilo_pass_org_processing_runs_id_fk": { + "name": "kilo_pass_org_notification_deliveries_processing_run_id_kilo_pass_org_processing_runs_id_fk", + "tableFrom": "kilo_pass_org_notification_deliveries", + "tableTo": "kilo_pass_org_processing_runs", + "columnsFrom": [ + "processing_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_notification_deliveries_recipient_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_notification_deliveries_recipient_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_notification_deliveries", + "tableTo": "kilocode_users", + "columnsFrom": [ + "recipient_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_notification_deliveries_run_recipient": { + "name": "UQ_kilo_pass_org_notification_deliveries_run_recipient", + "nullsNotDistinct": false, + "columns": [ + "processing_run_id", + "recipient_kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_notification_deliveries_status_check": { + "name": "kilo_pass_org_notification_deliveries_status_check", + "value": "\"kilo_pass_org_notification_deliveries\".\"status\" IN ('pending', 'sending', 'sent', 'failed')" + }, + "kilo_pass_org_notification_deliveries_attempt_count_check": { + "name": "kilo_pass_org_notification_deliveries_attempt_count_check", + "value": "\"kilo_pass_org_notification_deliveries\".\"attempt_count\" >= 0" + }, + "kilo_pass_org_notification_deliveries_sent_check": { + "name": "kilo_pass_org_notification_deliveries_sent_check", + "value": "(\"kilo_pass_org_notification_deliveries\".\"status\" = 'sent' AND \"kilo_pass_org_notification_deliveries\".\"sent_at\" IS NOT NULL AND \"kilo_pass_org_notification_deliveries\".\"lease_expires_at\" IS NULL) OR (\"kilo_pass_org_notification_deliveries\".\"status\" = 'sending' AND \"kilo_pass_org_notification_deliveries\".\"sent_at\" IS NULL AND \"kilo_pass_org_notification_deliveries\".\"lease_expires_at\" IS NOT NULL) OR (\"kilo_pass_org_notification_deliveries\".\"status\" IN ('pending', 'failed') AND \"kilo_pass_org_notification_deliveries\".\"sent_at\" IS NULL AND \"kilo_pass_org_notification_deliveries\".\"lease_expires_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_processing_runs": { + "name": "kilo_pass_org_processing_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "agreement_id": { + "name": "agreement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failure_code": { + "name": "failure_code", + "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": { + "IDX_kilo_pass_org_processing_runs_state_lease": { + "name": "IDX_kilo_pass_org_processing_runs_state_lease", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_processing_runs_agreement_id_kilo_pass_org_agreements_id_fk": { + "name": "kilo_pass_org_processing_runs_agreement_id_kilo_pass_org_agreements_id_fk", + "tableFrom": "kilo_pass_org_processing_runs", + "tableTo": "kilo_pass_org_agreements", + "columnsFrom": [ + "agreement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_processing_runs_agreement_window": { + "name": "UQ_kilo_pass_org_processing_runs_agreement_window", + "nullsNotDistinct": false, + "columns": [ + "agreement_id", + "window_start" + ] + }, + "UQ_kilo_pass_org_processing_runs_idempotency": { + "name": "UQ_kilo_pass_org_processing_runs_idempotency", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_processing_runs_window_check": { + "name": "kilo_pass_org_processing_runs_window_check", + "value": "\"kilo_pass_org_processing_runs\".\"window_start\" < \"kilo_pass_org_processing_runs\".\"window_end\"" + }, + "kilo_pass_org_processing_runs_attempt_count_non_negative_check": { + "name": "kilo_pass_org_processing_runs_attempt_count_non_negative_check", + "value": "\"kilo_pass_org_processing_runs\".\"attempt_count\" >= 0" + }, + "kilo_pass_org_processing_runs_state_check": { + "name": "kilo_pass_org_processing_runs_state_check", + "value": "\"kilo_pass_org_processing_runs\".\"state\" IN ('pending', 'running', 'succeeded', 'blocked', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_qualifying_spend_events": { + "name": "kilo_pass_org_qualifying_spend_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "issuance_snapshot_id": { + "name": "issuance_snapshot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "allocation_container_organization_id": { + "name": "allocation_container_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credit_transaction_id": { + "name": "credit_transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "spent_microdollars": { + "name": "spent_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "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": { + "IDX_kilo_pass_org_qualifying_spend_events_snapshot_occurred": { + "name": "IDX_kilo_pass_org_qualifying_spend_events_snapshot_occurred", + "columns": [ + { + "expression": "issuance_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_org_qualifying_spend_events_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk": { + "name": "kilo_pass_org_qualifying_spend_events_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk", + "tableFrom": "kilo_pass_org_qualifying_spend_events", + "tableTo": "kilo_pass_org_issuance_snapshots", + "columnsFrom": [ + "issuance_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_org_qualifying_spend_events_allocation_container_organization_id_organizations_id_fk": { + "name": "kilo_pass_org_qualifying_spend_events_allocation_container_organization_id_organizations_id_fk", + "tableFrom": "kilo_pass_org_qualifying_spend_events", + "tableTo": "organizations", + "columnsFrom": [ + "allocation_container_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "kilo_pass_org_qualifying_spend_events_credit_transaction_id_credit_transactions_id_fk": { + "name": "kilo_pass_org_qualifying_spend_events_credit_transaction_id_credit_transactions_id_fk", + "tableFrom": "kilo_pass_org_qualifying_spend_events", + "tableTo": "credit_transactions", + "columnsFrom": [ + "credit_transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_qualifying_spend_events_snapshot_credit_transaction": { + "name": "UQ_kilo_pass_org_qualifying_spend_events_snapshot_credit_transaction", + "nullsNotDistinct": false, + "columns": [ + "issuance_snapshot_id", + "credit_transaction_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_qualifying_spend_events_amount_positive_check": { + "name": "kilo_pass_org_qualifying_spend_events_amount_positive_check", + "value": "\"kilo_pass_org_qualifying_spend_events\".\"spent_microdollars\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_supplements": { + "name": "kilo_pass_org_supplements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "issuance_snapshot_id": { + "name": "issuance_snapshot_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider_invoice_line_id": { + "name": "provider_invoice_line_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remaining_service_numerator": { + "name": "remaining_service_numerator", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "remaining_service_denominator": { + "name": "remaining_service_denominator", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_supplements_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk": { + "name": "kilo_pass_org_supplements_issuance_snapshot_id_kilo_pass_org_issuance_snapshots_id_fk", + "tableFrom": "kilo_pass_org_supplements", + "tableTo": "kilo_pass_org_issuance_snapshots", + "columnsFrom": [ + "issuance_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_supplements_provider_invoice_line": { + "name": "UQ_kilo_pass_org_supplements_provider_invoice_line", + "nullsNotDistinct": false, + "columns": [ + "provider_invoice_line_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_supplements_ratio_check": { + "name": "kilo_pass_org_supplements_ratio_check", + "value": "\"kilo_pass_org_supplements\".\"remaining_service_numerator\" > 0 AND \"kilo_pass_org_supplements\".\"remaining_service_denominator\" > 0 AND \"kilo_pass_org_supplements\".\"remaining_service_numerator\" <= \"kilo_pass_org_supplements\".\"remaining_service_denominator\"" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_org_term_versions": { + "name": "kilo_pass_org_term_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "version_key": { + "name": "version_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_price_microdollars_per_pass": { + "name": "billing_price_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "base_credit_microdollars_per_pass": { + "name": "base_credit_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bonus_credit_microdollars_per_pass": { + "name": "bonus_credit_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "unlock_spend_microdollars_per_pass": { + "name": "unlock_spend_microdollars_per_pass", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bonus_mode": { + "name": "bonus_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kilo_pass_org_term_versions_created_by_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_org_term_versions_created_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_org_term_versions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_kilo_pass_org_term_versions_version_key": { + "name": "UQ_kilo_pass_org_term_versions_version_key", + "nullsNotDistinct": false, + "columns": [ + "version_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_org_term_versions_amounts_non_negative_check": { + "name": "kilo_pass_org_term_versions_amounts_non_negative_check", + "value": "\"kilo_pass_org_term_versions\".\"billing_price_microdollars_per_pass\" >= 0 AND \"kilo_pass_org_term_versions\".\"base_credit_microdollars_per_pass\" >= 0 AND \"kilo_pass_org_term_versions\".\"bonus_credit_microdollars_per_pass\" >= 0 AND \"kilo_pass_org_term_versions\".\"unlock_spend_microdollars_per_pass\" >= 0" + }, + "kilo_pass_org_term_versions_tier_check": { + "name": "kilo_pass_org_term_versions_tier_check", + "value": "\"kilo_pass_org_term_versions\".\"tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_org_term_versions_cadence_check": { + "name": "kilo_pass_org_term_versions_cadence_check", + "value": "\"kilo_pass_org_term_versions\".\"cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_org_term_versions_bonus_mode_check": { + "name": "kilo_pass_org_term_versions_bonus_mode_check", + "value": "\"kilo_pass_org_term_versions\".\"bonus_mode\" IN ('after_base', 'upfront')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_pause_events": { + "name": "kilo_pass_pause_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resumes_at": { + "name": "resumes_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resumed_at": { + "name": "resumed_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": { + "IDX_kilo_pass_pause_events_subscription_id": { + "name": "IDX_kilo_pass_pause_events_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_pause_events_one_open_per_sub": { + "name": "UQ_kilo_pass_pause_events_one_open_per_sub", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_pause_events\".\"resumed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_pause_events_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_pause_events_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_pause_events", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_pause_events_resumed_at_after_paused_at_check": { + "name": "kilo_pass_pause_events_resumed_at_after_paused_at_check", + "value": "\"kilo_pass_pause_events\".\"resumed_at\" IS NULL OR \"kilo_pass_pause_events\".\"resumed_at\" >= \"kilo_pass_pause_events\".\"paused_at\"" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_scheduled_changes": { + "name": "kilo_pass_scheduled_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_tier": { + "name": "from_tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_cadence": { + "name": "from_cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_tier": { + "name": "to_tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_cadence": { + "name": "to_cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_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": { + "IDX_kilo_pass_scheduled_changes_kilo_user_id": { + "name": "IDX_kilo_pass_scheduled_changes_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_status": { + "name": "IDX_kilo_pass_scheduled_changes_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_stripe_subscription_id": { + "name": "IDX_kilo_pass_scheduled_changes_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_scheduled_changes_active_stripe_subscription_id": { + "name": "UQ_kilo_pass_scheduled_changes_active_stripe_subscription_id", + "columns": [ + { + "expression": "stripe_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_scheduled_changes\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_effective_at": { + "name": "IDX_kilo_pass_scheduled_changes_effective_at", + "columns": [ + { + "expression": "effective_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_scheduled_changes_deleted_at": { + "name": "IDX_kilo_pass_scheduled_changes_deleted_at", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_scheduled_changes_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_scheduled_changes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_scheduled_changes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_scheduled_changes_stripe_subscription_id_kilo_pass_subscriptions_stripe_subscription_id_fk": { + "name": "kilo_pass_scheduled_changes_stripe_subscription_id_kilo_pass_subscriptions_stripe_subscription_id_fk", + "tableFrom": "kilo_pass_scheduled_changes", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "stripe_subscription_id" + ], + "columnsTo": [ + "stripe_subscription_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_scheduled_changes_from_tier_check": { + "name": "kilo_pass_scheduled_changes_from_tier_check", + "value": "\"kilo_pass_scheduled_changes\".\"from_tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_scheduled_changes_from_cadence_check": { + "name": "kilo_pass_scheduled_changes_from_cadence_check", + "value": "\"kilo_pass_scheduled_changes\".\"from_cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_scheduled_changes_to_tier_check": { + "name": "kilo_pass_scheduled_changes_to_tier_check", + "value": "\"kilo_pass_scheduled_changes\".\"to_tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_scheduled_changes_to_cadence_check": { + "name": "kilo_pass_scheduled_changes_to_cadence_check", + "value": "\"kilo_pass_scheduled_changes\".\"to_cadence\" IN ('monthly', 'yearly')" + }, + "kilo_pass_scheduled_changes_status_check": { + "name": "kilo_pass_scheduled_changes_status_check", + "value": "\"kilo_pass_scheduled_changes\".\"status\" IN ('not_started', 'active', 'completed', 'released', 'canceled')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_store_events": { + "name": "kilo_pass_store_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_transaction_id": { + "name": "provider_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_account_token": { + "name": "app_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_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": { + "UQ_kilo_pass_store_events_provider_event": { + "name": "UQ_kilo_pass_store_events_provider_event", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_events_provider_subscription": { + "name": "IDX_kilo_pass_store_events_provider_subscription", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_events_app_account_token": { + "name": "IDX_kilo_pass_store_events_app_account_token", + "columns": [ + { + "expression": "app_account_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_store_events_payment_provider_check": { + "name": "kilo_pass_store_events_payment_provider_check", + "value": "\"kilo_pass_store_events\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_store_purchases": { + "name": "kilo_pass_store_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_pass_subscription_id": { + "name": "kilo_pass_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_transaction_id": { + "name": "provider_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_original_transaction_id": { + "name": "provider_original_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_account_token": { + "name": "app_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "purchase_token": { + "name": "purchase_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchased_at": { + "name": "purchased_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "amount_charged_minor_units": { + "name": "amount_charged_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tax_minor_units": { + "name": "tax_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "money_backfill_attempted_at": { + "name": "money_backfill_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "raw_payload_json": { + "name": "raw_payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "UQ_kilo_pass_store_purchases_provider_transaction": { + "name": "UQ_kilo_pass_store_purchases_provider_transaction", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_subscription_id": { + "name": "IDX_kilo_pass_store_purchases_subscription_id", + "columns": [ + { + "expression": "kilo_pass_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_user_id": { + "name": "IDX_kilo_pass_store_purchases_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_app_account_token": { + "name": "IDX_kilo_pass_store_purchases_app_account_token", + "columns": [ + { + "expression": "app_account_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_store_purchases_latest_subscription_purchase": { + "name": "IDX_kilo_pass_store_purchases_latest_subscription_purchase", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "purchased_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_store_purchases_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk": { + "name": "kilo_pass_store_purchases_kilo_pass_subscription_id_kilo_pass_subscriptions_id_fk", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "kilo_pass_store_purchases_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_store_purchases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "FK_kilo_pass_store_purchases_subscription_owner_provider": { + "name": "FK_kilo_pass_store_purchases_subscription_owner_provider", + "tableFrom": "kilo_pass_store_purchases", + "tableTo": "kilo_pass_subscriptions", + "columnsFrom": [ + "kilo_pass_subscription_id", + "kilo_user_id", + "payment_provider", + "provider_subscription_id" + ], + "columnsTo": [ + "id", + "kilo_user_id", + "payment_provider", + "provider_subscription_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kilo_pass_store_purchases_store_provider_check": { + "name": "kilo_pass_store_purchases_store_provider_check", + "value": "\"kilo_pass_store_purchases\".\"payment_provider\" IN ('app_store', 'google_play')" + }, + "kilo_pass_store_purchases_amount_charged_non_negative_check": { + "name": "kilo_pass_store_purchases_amount_charged_non_negative_check", + "value": "\"kilo_pass_store_purchases\".\"amount_charged_minor_units\" IS NULL OR \"kilo_pass_store_purchases\".\"amount_charged_minor_units\" >= 0" + }, + "kilo_pass_store_purchases_tax_non_negative_check": { + "name": "kilo_pass_store_purchases_tax_non_negative_check", + "value": "\"kilo_pass_store_purchases\".\"tax_minor_units\" IS NULL OR \"kilo_pass_store_purchases\".\"tax_minor_units\" >= 0" + }, + "kilo_pass_store_purchases_currency_check": { + "name": "kilo_pass_store_purchases_currency_check", + "value": "\"kilo_pass_store_purchases\".\"currency\" IS NULL OR \"kilo_pass_store_purchases\".\"currency\" ~ '^[A-Z]{3}$'" + }, + "kilo_pass_store_purchases_currency_required_check": { + "name": "kilo_pass_store_purchases_currency_required_check", + "value": "\"kilo_pass_store_purchases\".\"currency\" IS NOT NULL OR (\"kilo_pass_store_purchases\".\"amount_charged_minor_units\" IS NULL AND \"kilo_pass_store_purchases\".\"tax_minor_units\" IS NULL)" + }, + "kilo_pass_store_purchases_payment_provider_check": { + "name": "kilo_pass_store_purchases_payment_provider_check", + "value": "\"kilo_pass_store_purchases\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_subscriptions": { + "name": "kilo_pass_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_provider": { + "name": "payment_provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'stripe'" + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cadence": { + "name": "cadence", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_streak_months": { + "name": "current_streak_months", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_yearly_issue_at": { + "name": "next_yearly_issue_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": { + "IDX_kilo_pass_subscriptions_kilo_user_id": { + "name": "IDX_kilo_pass_subscriptions_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_payment_provider": { + "name": "IDX_kilo_pass_subscriptions_payment_provider", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_status": { + "name": "IDX_kilo_pass_subscriptions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilo_pass_subscriptions_cadence": { + "name": "IDX_kilo_pass_subscriptions_cadence", + "columns": [ + { + "expression": "cadence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_subscriptions_provider_subscription": { + "name": "UQ_kilo_pass_subscriptions_provider_subscription", + "columns": [ + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilo_pass_subscriptions_store_purchase_reference": { + "name": "UQ_kilo_pass_subscriptions_store_purchase_reference", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payment_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kilo_pass_subscriptions_kilo_user_id_kilocode_users_id_fk": { + "name": "kilo_pass_subscriptions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kilo_pass_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilo_pass_subscriptions_stripe_subscription_id_unique": { + "name": "kilo_pass_subscriptions_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_subscriptions_current_streak_months_non_negative_check": { + "name": "kilo_pass_subscriptions_current_streak_months_non_negative_check", + "value": "\"kilo_pass_subscriptions\".\"current_streak_months\" >= 0" + }, + "kilo_pass_subscriptions_provider_ids_check": { + "name": "kilo_pass_subscriptions_provider_ids_check", + "value": "(\n \"kilo_pass_subscriptions\".\"payment_provider\" = 'stripe'\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"stripe_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" = \"kilo_pass_subscriptions\".\"stripe_subscription_id\"\n ) OR (\n \"kilo_pass_subscriptions\".\"payment_provider\" IN ('app_store', 'google_play')\n AND \"kilo_pass_subscriptions\".\"provider_subscription_id\" IS NOT NULL\n AND \"kilo_pass_subscriptions\".\"stripe_subscription_id\" IS NULL\n )" + }, + "kilo_pass_subscriptions_payment_provider_check": { + "name": "kilo_pass_subscriptions_payment_provider_check", + "value": "\"kilo_pass_subscriptions\".\"payment_provider\" IN ('stripe', 'app_store', 'google_play')" + }, + "kilo_pass_subscriptions_tier_check": { + "name": "kilo_pass_subscriptions_tier_check", + "value": "\"kilo_pass_subscriptions\".\"tier\" IN ('tier_19', 'tier_49', 'tier_199')" + }, + "kilo_pass_subscriptions_cadence_check": { + "name": "kilo_pass_subscriptions_cadence_check", + "value": "\"kilo_pass_subscriptions\".\"cadence\" IN ('monthly', 'yearly')" + } + }, + "isRLSEnabled": false + }, + "public.kilo_pass_welcome_promo_payment_fingerprint_claims": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims", + "schema": "", + "columns": { + "stripe_payment_method_type": { + "name": "stripe_payment_method_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_fingerprint": { + "name": "stripe_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_stripe_invoice_id": { + "name": "source_stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "kilo_pass_welcome_promo_payment_fingerprint_claims_stripe_payment_method_type_stripe_fingerprint_pk": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims_stripe_payment_method_type_stripe_fingerprint_pk", + "columns": [ + "stripe_payment_method_type", + "stripe_fingerprint" + ] + } + }, + "uniqueConstraints": { + "UQ_kilo_pass_welcome_promo_payment_fingerprint_claims_source_invoice_id": { + "name": "UQ_kilo_pass_welcome_promo_payment_fingerprint_claims_source_invoice_id", + "nullsNotDistinct": false, + "columns": [ + "source_stripe_invoice_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kilo_pass_welcome_promo_payment_fingerprint_claims_type_check": { + "name": "kilo_pass_welcome_promo_payment_fingerprint_claims_type_check", + "value": "\"kilo_pass_welcome_promo_payment_fingerprint_claims\".\"stripe_payment_method_type\" IN ('card', 'sepa_debit', 'us_bank_account', 'bacs_debit', 'au_becs_debit')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_access_codes": { + "name": "kiloclaw_access_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "redeemed_at": { + "name": "redeemed_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": { + "UQ_kiloclaw_access_codes_code": { + "name": "UQ_kiloclaw_access_codes_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_access_codes_user_status": { + "name": "IDX_kiloclaw_access_codes_user_status", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_access_codes_one_active_per_user": { + "name": "UQ_kiloclaw_access_codes_one_active_per_user", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_access_codes_kilo_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_access_codes_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_access_codes", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_admin_audit_logs": { + "name": "kiloclaw_admin_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_kiloclaw_admin_audit_logs_target_user_id": { + "name": "IDX_kiloclaw_admin_audit_logs_target_user_id", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_admin_audit_logs_action": { + "name": "IDX_kiloclaw_admin_audit_logs_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_admin_audit_logs_created_at": { + "name": "IDX_kiloclaw_admin_audit_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_cli_runs": { + "name": "kiloclaw_cli_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "initiated_by_admin_id": { + "name": "initiated_by_admin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "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": { + "IDX_kiloclaw_cli_runs_user_id": { + "name": "IDX_kiloclaw_cli_runs_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_cli_runs_started_at": { + "name": "IDX_kiloclaw_cli_runs_started_at", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_cli_runs_instance_id": { + "name": "IDX_kiloclaw_cli_runs_instance_id", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_cli_runs_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_cli_runs_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_cli_runs_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_cli_runs_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_cli_runs_initiated_by_admin_id_kilocode_users_id_fk": { + "name": "kiloclaw_cli_runs_initiated_by_admin_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_cli_runs", + "tableTo": "kilocode_users", + "columnsFrom": [ + "initiated_by_admin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_earlybird_purchases": { + "name": "kiloclaw_earlybird_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manual_payment_id": { + "name": "manual_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "kiloclaw_earlybird_purchases_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_earlybird_purchases_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_earlybird_purchases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_earlybird_purchases_user_id_unique": { + "name": "kiloclaw_earlybird_purchases_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "kiloclaw_earlybird_purchases_stripe_charge_id_unique": { + "name": "kiloclaw_earlybird_purchases_stripe_charge_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_charge_id" + ] + }, + "kiloclaw_earlybird_purchases_manual_payment_id_unique": { + "name": "kiloclaw_earlybird_purchases_manual_payment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "manual_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_email_log": { + "name": "kiloclaw_email_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_type": { + "name": "email_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "'epoch'" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_kiloclaw_email_log_user_type_global": { + "name": "UQ_kiloclaw_email_log_user_type_global", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_email_log\".\"instance_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_email_log_user_instance_type_period": { + "name": "UQ_kiloclaw_email_log_user_instance_type_period", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_email_log\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_email_log_type_sent_instance": { + "name": "IDX_kiloclaw_email_log_type_sent_instance", + "columns": [ + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sent_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_email_log\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_email_log_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_email_log_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_email_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_email_log_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_email_log_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_email_log", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_google_oauth_connections": { + "name": "kiloclaw_google_oauth_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'google'" + }, + "account_email": { + "name": "account_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_subject": { + "name": "account_subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_secret_encrypted": { + "name": "oauth_client_secret_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_profile": { + "name": "credential_profile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'kilo_owned'" + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "grants_by_source": { + "name": "grants_by_source", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "capabilities": { + "name": "capabilities", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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": { + "UQ_kiloclaw_google_oauth_connections_instance": { + "name": "UQ_kiloclaw_google_oauth_connections_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_google_oauth_connections_status": { + "name": "IDX_kiloclaw_google_oauth_connections_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_google_oauth_connections_provider": { + "name": "IDX_kiloclaw_google_oauth_connections_provider", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_google_oauth_connections_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_google_oauth_connections_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_google_oauth_connections", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_google_oauth_connections_status_check": { + "name": "kiloclaw_google_oauth_connections_status_check", + "value": "\"kiloclaw_google_oauth_connections\".\"status\" IN ('active', 'action_required', 'disconnected')" + }, + "kiloclaw_google_oauth_connections_credential_profile_check": { + "name": "kiloclaw_google_oauth_connections_credential_profile_check", + "value": "\"kiloclaw_google_oauth_connections\".\"credential_profile\" IN ('legacy', 'kilo_owned')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_image_catalog": { + "name": "kiloclaw_image_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "openclaw_version": { + "name": "openclaw_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_digest": { + "name": "image_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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()" + }, + "rollout_percent": { + "name": "rollout_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_latest": { + "name": "is_latest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_kiloclaw_image_catalog_status": { + "name": "IDX_kiloclaw_image_catalog_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_image_catalog_variant": { + "name": "IDX_kiloclaw_image_catalog_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_image_catalog_one_latest_per_variant": { + "name": "UQ_kiloclaw_image_catalog_one_latest_per_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_image_catalog\".\"is_latest\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_image_catalog_one_candidate_per_variant": { + "name": "UQ_kiloclaw_image_catalog_one_candidate_per_variant", + "columns": [ + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_image_catalog\".\"is_latest\" = false AND \"kiloclaw_image_catalog\".\"rollout_percent\" > 0 AND \"kiloclaw_image_catalog\".\"status\" = 'available'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_image_catalog_image_tag_unique": { + "name": "kiloclaw_image_catalog_image_tag_unique", + "nullsNotDistinct": false, + "columns": [ + "image_tag" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_inbound_email_aliases": { + "name": "kiloclaw_inbound_email_aliases", + "schema": "", + "columns": { + "alias": { + "name": "alias", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_inbound_email_aliases_instance_id": { + "name": "IDX_kiloclaw_inbound_email_aliases_instance_id", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_inbound_email_aliases_active_instance": { + "name": "UQ_kiloclaw_inbound_email_aliases_active_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_inbound_email_aliases\".\"retired_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_inbound_email_aliases_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_inbound_email_aliases_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_inbound_email_aliases", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_inbound_email_reserved_aliases": { + "name": "kiloclaw_inbound_email_reserved_aliases", + "schema": "", + "columns": { + "alias": { + "name": "alias", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_instances": { + "name": "kiloclaw_instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_id": { + "name": "sandbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'fly'" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbound_email_enabled": { + "name": "inbound_email_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inactive_trial_stopped_at": { + "name": "inactive_trial_stopped_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()" + }, + "destroyed_at": { + "name": "destroyed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tracked_image_tag": { + "name": "tracked_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instance_type": { + "name": "instance_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_size_override": { + "name": "admin_size_override", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_instances_active": { + "name": "UQ_kiloclaw_instances_active", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sandbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_personal_by_user": { + "name": "IDX_kiloclaw_instances_active_personal_by_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_org_by_user_org": { + "name": "IDX_kiloclaw_instances_active_org_by_user_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_active_org_by_org_created": { + "name": "IDX_kiloclaw_instances_active_org_by_org_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"organization_id\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_user_id_created_at": { + "name": "IDX_kiloclaw_instances_user_id_created_at", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_tracked_image_tag": { + "name": "IDX_kiloclaw_instances_tracked_image_tag", + "columns": [ + { + "expression": "tracked_image_tag", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_instance_type": { + "name": "IDX_kiloclaw_instances_instance_type", + "columns": [ + { + "expression": "instance_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"destroyed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_instances_admin_size_override": { + "name": "IDX_kiloclaw_instances_admin_size_override", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_instances\".\"admin_size_override\" IS NOT NULL AND \"kiloclaw_instances\".\"destroyed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_instances_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_instances_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_instances", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_instances_organization_id_organizations_id_fk": { + "name": "kiloclaw_instances_organization_id_organizations_id_fk", + "tableFrom": "kiloclaw_instances", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_kiloclaw_instances_instance_type": { + "name": "CHK_kiloclaw_instances_instance_type", + "value": "\"kiloclaw_instances\".\"instance_type\" IS NULL OR \"kiloclaw_instances\".\"instance_type\" IN ('perf-1-3', 'perf-4-8', 'perf-4-16', 'shared-2-3', 'shared-2-4', 'custom')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_morning_briefing_configs": { + "name": "kiloclaw_morning_briefing_configs", + "schema": "", + "columns": { + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cron": { + "name": "cron", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'0 7 * * *'" + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "interest_topics": { + "name": "interest_topics", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "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": { + "IDX_kiloclaw_morning_briefing_configs_enabled": { + "name": "IDX_kiloclaw_morning_briefing_configs_enabled", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_morning_briefing_configs\".\"enabled\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_morning_briefing_configs_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_morning_briefing_configs_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_morning_briefing_configs", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_notifications": { + "name": "kiloclaw_scheduled_action_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'notice'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_notifications_target_kind_channel": { + "name": "UQ_kiloclaw_scheduled_action_notifications_target_kind_channel", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_notifications_pending": { + "name": "IDX_kiloclaw_scheduled_action_notifications_pending", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_notifications\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_notifications_target_id_kiloclaw_scheduled_action_targets_id_fk": { + "name": "kiloclaw_scheduled_action_notifications_target_id_kiloclaw_scheduled_action_targets_id_fk", + "tableFrom": "kiloclaw_scheduled_action_notifications", + "tableTo": "kiloclaw_scheduled_action_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_stages": { + "name": "kiloclaw_scheduled_action_stages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scheduled_action_id": { + "name": "scheduled_action_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_index": { + "name": "stage_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "notice_sent_at": { + "name": "notice_sent_at", + "type": "timestamp with time zone", + "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 + }, + "applied_count": { + "name": "applied_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_stages_parent_index": { + "name": "UQ_kiloclaw_scheduled_action_stages_parent_index", + "columns": [ + { + "expression": "scheduled_action_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_stages_notice_due": { + "name": "IDX_kiloclaw_scheduled_action_stages_notice_due", + "columns": [ + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_stages\".\"notice_sent_at\" IS NULL AND \"kiloclaw_scheduled_action_stages\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_stages_scheduled_action_id_kiloclaw_scheduled_actions_id_fk": { + "name": "kiloclaw_scheduled_action_stages_scheduled_action_id_kiloclaw_scheduled_actions_id_fk", + "tableFrom": "kiloclaw_scheduled_action_stages", + "tableTo": "kiloclaw_scheduled_actions", + "columnsFrom": [ + "scheduled_action_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_action_targets": { + "name": "kiloclaw_scheduled_action_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scheduled_action_id": { + "name": "scheduled_action_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_image_tag": { + "name": "source_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_image_tag": { + "name": "target_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_kiloclaw_scheduled_action_targets_parent_instance": { + "name": "UQ_kiloclaw_scheduled_action_targets_parent_instance", + "columns": [ + { + "expression": "scheduled_action_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_targets_stage": { + "name": "IDX_kiloclaw_scheduled_action_targets_stage", + "columns": [ + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_action_targets_pending_by_instance": { + "name": "IDX_kiloclaw_scheduled_action_targets_pending_by_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_scheduled_action_targets\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_action_targets_scheduled_action_id_kiloclaw_scheduled_actions_id_fk": { + "name": "kiloclaw_scheduled_action_targets_scheduled_action_id_kiloclaw_scheduled_actions_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_scheduled_actions", + "columnsFrom": [ + "scheduled_action_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_stage_id_kiloclaw_scheduled_action_stages_id_fk": { + "name": "kiloclaw_scheduled_action_targets_stage_id_kiloclaw_scheduled_action_stages_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_scheduled_action_stages", + "columnsFrom": [ + "stage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_scheduled_action_targets_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_action_targets_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_scheduled_action_targets_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_scheduled_action_targets", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_scheduled_actions": { + "name": "kiloclaw_scheduled_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_image_tag": { + "name": "target_image_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "override_pins": { + "name": "override_pins", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notice_lead_hours": { + "name": "notice_lead_hours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 24 + }, + "notice_subject": { + "name": "notice_subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "notice_body": { + "name": "notice_body", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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 + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "total_count": { + "name": "total_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "applied_count": { + "name": "applied_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped_count": { + "name": "skipped_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "IDX_kiloclaw_scheduled_actions_status": { + "name": "IDX_kiloclaw_scheduled_actions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_actions_action_type": { + "name": "IDX_kiloclaw_scheduled_actions_action_type", + "columns": [ + { + "expression": "action_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_scheduled_actions_created_by": { + "name": "IDX_kiloclaw_scheduled_actions_created_by", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_scheduled_actions_target_image_tag_kiloclaw_image_catalog_image_tag_fk": { + "name": "kiloclaw_scheduled_actions_target_image_tag_kiloclaw_image_catalog_image_tag_fk", + "tableFrom": "kiloclaw_scheduled_actions", + "tableTo": "kiloclaw_image_catalog", + "columnsFrom": [ + "target_image_tag" + ], + "columnsTo": [ + "image_tag" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "kiloclaw_scheduled_actions_created_by_kilocode_users_id_fk": { + "name": "kiloclaw_scheduled_actions_created_by_kilocode_users_id_fk", + "tableFrom": "kiloclaw_scheduled_actions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kiloclaw_subscription_change_log": { + "name": "kiloclaw_subscription_change_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "before_state": { + "name": "before_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_state": { + "name": "after_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_kiloclaw_subscription_change_log_subscription_created_at": { + "name": "IDX_kiloclaw_subscription_change_log_subscription_created_at", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscription_change_log_created_at": { + "name": "IDX_kiloclaw_subscription_change_log_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_subscription_change_log_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_subscription_change_log_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_subscription_change_log", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_subscription_change_log_actor_type_check": { + "name": "kiloclaw_subscription_change_log_actor_type_check", + "value": "\"kiloclaw_subscription_change_log\".\"actor_type\" IN ('user', 'system')" + }, + "kiloclaw_subscription_change_log_action_check": { + "name": "kiloclaw_subscription_change_log_action_check", + "value": "\"kiloclaw_subscription_change_log\".\"action\" IN ('created', 'status_changed', 'plan_switched', 'period_advanced', 'canceled', 'reactivated', 'suspended', 'destruction_scheduled', 'reassigned', 'backfilled', 'payment_source_changed', 'schedule_changed', 'admin_override')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_subscriptions": { + "name": "kiloclaw_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transferred_to_subscription_id": { + "name": "transferred_to_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "access_origin": { + "name": "access_origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_source": { + "name": "payment_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kiloclaw_price_version": { + "name": "kiloclaw_price_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_plan": { + "name": "scheduled_plan", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_by": { + "name": "scheduled_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pending_conversion": { + "name": "pending_conversion", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trial_started_at": { + "name": "trial_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credit_renewal_at": { + "name": "credit_renewal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "commit_ends_at": { + "name": "commit_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "destruction_deadline": { + "name": "destruction_deadline", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_requested_at": { + "name": "auto_resume_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_retry_after": { + "name": "auto_resume_retry_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_resume_attempt_count": { + "name": "auto_resume_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "auto_top_up_triggered_for_period": { + "name": "auto_top_up_triggered_for_period", + "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": { + "IDX_kiloclaw_subscriptions_status": { + "name": "IDX_kiloclaw_subscriptions_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_user_id": { + "name": "IDX_kiloclaw_subscriptions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_user_status": { + "name": "IDX_kiloclaw_subscriptions_user_status", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_price_version": { + "name": "IDX_kiloclaw_subscriptions_price_version", + "columns": [ + { + "expression": "kiloclaw_price_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_transferred_to": { + "name": "IDX_kiloclaw_subscriptions_transferred_to", + "columns": [ + { + "expression": "transferred_to_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_stripe_schedule_id": { + "name": "IDX_kiloclaw_subscriptions_stripe_schedule_id", + "columns": [ + { + "expression": "stripe_schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_auto_resume_retry_after": { + "name": "IDX_kiloclaw_subscriptions_auto_resume_retry_after", + "columns": [ + { + "expression": "auto_resume_retry_after", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_subscriptions_instance": { + "name": "UQ_kiloclaw_subscriptions_instance", + "columns": [ + { + "expression": "instance_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_subscriptions\".\"instance_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kiloclaw_subscriptions_transferred_to": { + "name": "UQ_kiloclaw_subscriptions_transferred_to", + "columns": [ + { + "expression": "transferred_to_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kiloclaw_subscriptions\".\"transferred_to_subscription_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_subscriptions_earlybird_origin": { + "name": "IDX_kiloclaw_subscriptions_earlybird_origin", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "access_origin", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_subscriptions\".\"access_origin\" = 'earlybird'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_subscriptions_user_id_kilocode_users_id_fk": { + "name": "kiloclaw_subscriptions_user_id_kilocode_users_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_subscriptions_transferred_to_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_subscriptions_transferred_to_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "transferred_to_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_subscriptions_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_subscriptions_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_subscriptions", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_subscriptions_stripe_subscription_id_unique": { + "name": "kiloclaw_subscriptions_stripe_subscription_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_subscription_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "kiloclaw_subscriptions_price_version_check": { + "name": "kiloclaw_subscriptions_price_version_check", + "value": "\"kiloclaw_subscriptions\".\"kiloclaw_price_version\" IN ('2026-03-19', '2026-05-10')" + }, + "kiloclaw_subscriptions_plan_check": { + "name": "kiloclaw_subscriptions_plan_check", + "value": "\"kiloclaw_subscriptions\".\"plan\" IN ('trial', 'commit', 'standard')" + }, + "kiloclaw_subscriptions_scheduled_plan_check": { + "name": "kiloclaw_subscriptions_scheduled_plan_check", + "value": "\"kiloclaw_subscriptions\".\"scheduled_plan\" IN ('commit', 'standard')" + }, + "kiloclaw_subscriptions_scheduled_by_check": { + "name": "kiloclaw_subscriptions_scheduled_by_check", + "value": "\"kiloclaw_subscriptions\".\"scheduled_by\" IN ('auto', 'user')" + }, + "kiloclaw_subscriptions_status_check": { + "name": "kiloclaw_subscriptions_status_check", + "value": "\"kiloclaw_subscriptions\".\"status\" IN ('trialing', 'active', 'past_due', 'canceled', 'unpaid')" + }, + "kiloclaw_subscriptions_access_origin_check": { + "name": "kiloclaw_subscriptions_access_origin_check", + "value": "\"kiloclaw_subscriptions\".\"access_origin\" IN ('earlybird')" + }, + "kiloclaw_subscriptions_payment_source_check": { + "name": "kiloclaw_subscriptions_payment_source_check", + "value": "\"kiloclaw_subscriptions\".\"payment_source\" IN ('stripe', 'credits')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_terminal_renewal_failures": { + "name": "kiloclaw_terminal_renewal_failures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "renewal_boundary": { + "name": "renewal_boundary", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unresolved'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_failure_at": { + "name": "first_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_failure_at": { + "name": "last_failure_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_failure_code": { + "name": "last_failure_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_failure_message": { + "name": "last_failure_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_actor_type": { + "name": "resolution_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_actor_id": { + "name": "resolution_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_at": { + "name": "resolution_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution_reason": { + "name": "resolution_reason", + "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": { + "UQ_kiloclaw_terminal_renewal_failures_subscription_boundary": { + "name": "UQ_kiloclaw_terminal_renewal_failures_subscription_boundary", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "renewal_boundary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_terminal_renewal_failures_unresolved": { + "name": "IDX_kiloclaw_terminal_renewal_failures_unresolved", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "renewal_boundary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"kiloclaw_terminal_renewal_failures\".\"status\" = 'unresolved'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kiloclaw_terminal_renewal_failures_status_last_failure_at": { + "name": "IDX_kiloclaw_terminal_renewal_failures_status_last_failure_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_failure_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kiloclaw_terminal_renewal_failures_subscription_id_kiloclaw_subscriptions_id_fk": { + "name": "kiloclaw_terminal_renewal_failures_subscription_id_kiloclaw_subscriptions_id_fk", + "tableFrom": "kiloclaw_terminal_renewal_failures", + "tableTo": "kiloclaw_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kiloclaw_terminal_renewal_failures_status_check": { + "name": "kiloclaw_terminal_renewal_failures_status_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"status\" IN ('unresolved', 'resolved', 'waived', 'superseded')" + }, + "kiloclaw_terminal_renewal_failures_last_failure_code_check": { + "name": "kiloclaw_terminal_renewal_failures_last_failure_code_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"last_failure_code\" IN ('credit_balance_read_failed', 'renewal_transaction_failed', 'auto_top_up_marker_write_failed', 'worker_timeout', 'poison_payload', 'queue_delivery_exhausted')" + }, + "kiloclaw_terminal_renewal_failures_resolution_actor_type_check": { + "name": "kiloclaw_terminal_renewal_failures_resolution_actor_type_check", + "value": "\"kiloclaw_terminal_renewal_failures\".\"resolution_actor_type\" IN ('operator', 'system')" + } + }, + "isRLSEnabled": false + }, + "public.kiloclaw_version_pins": { + "name": "kiloclaw_version_pins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "instance_id": { + "name": "instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "image_tag": { + "name": "image_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_by": { + "name": "pinned_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "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": {}, + "foreignKeys": { + "kiloclaw_version_pins_instance_id_kiloclaw_instances_id_fk": { + "name": "kiloclaw_version_pins_instance_id_kiloclaw_instances_id_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kiloclaw_instances", + "columnsFrom": [ + "instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "kiloclaw_version_pins_image_tag_kiloclaw_image_catalog_image_tag_fk": { + "name": "kiloclaw_version_pins_image_tag_kiloclaw_image_catalog_image_tag_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kiloclaw_image_catalog", + "columnsFrom": [ + "image_tag" + ], + "columnsTo": [ + "image_tag" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "kiloclaw_version_pins_pinned_by_kilocode_users_id_fk": { + "name": "kiloclaw_version_pins_pinned_by_kilocode_users_id_fk", + "tableFrom": "kiloclaw_version_pins", + "tableTo": "kilocode_users", + "columnsFrom": [ + "pinned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kiloclaw_version_pins_instance_id_unique": { + "name": "kiloclaw_version_pins_instance_id_unique", + "nullsNotDistinct": false, + "columns": [ + "instance_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.kilocode_users": { + "name": "kilocode_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "google_user_email": { + "name": "google_user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_user_name": { + "name": "google_user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "google_user_image_url": { + "name": "google_user_image_url", + "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()" + }, + "hosted_domain": { + "name": "hosted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "kilo_pass_threshold": { + "name": "kilo_pass_threshold", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_store_account_token": { + "name": "app_store_account_token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_super_admin": { + "name": "is_super_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_view_sessions": { + "name": "can_view_sessions", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_manage_credits": { + "name": "can_manage_credits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "total_microdollars_acquired": { + "name": "total_microdollars_acquired", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "next_credit_expiration_at": { + "name": "next_credit_expiration_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "has_validation_stytch": { + "name": "has_validation_stytch", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_validation_novel_card_with_hold": { + "name": "has_validation_novel_card_with_hold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_at": { + "name": "blocked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_by_kilo_user_id": { + "name": "blocked_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_token_pepper": { + "name": "api_token_pepper", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_session_pepper": { + "name": "web_session_pepper", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_enabled": { + "name": "auto_top_up_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "kiloclaw_early_access": { + "name": "kiloclaw_early_access", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_model": { + "name": "default_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cohorts": { + "name": "cohorts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "completed_welcome_form": { + "name": "completed_welcome_form", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "linkedin_url": { + "name": "linkedin_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_url": { + "name": "github_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discord_server_membership_verified_at": { + "name": "discord_server_membership_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "openrouter_upstream_safety_identifier": { + "name": "openrouter_upstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "openrouter_downstream_safety_identifier": { + "name": "openrouter_downstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vercel_downstream_safety_identifier": { + "name": "vercel_downstream_safety_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_source": { + "name": "customer_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signup_ip": { + "name": "signup_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_deletion_requested_at": { + "name": "account_deletion_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_domain": { + "name": "email_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "personal_account_disabled": { + "name": "personal_account_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "IDX_kilocode_users_signup_ip_created_at": { + "name": "IDX_kilocode_users_signup_ip_created_at", + "columns": [ + { + "expression": "signup_ip", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_blocked_at": { + "name": "IDX_kilocode_users_blocked_at", + "columns": [ + { + "expression": "blocked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_blocked_by_kilo_user_id": { + "name": "IDX_kilocode_users_blocked_by_kilo_user_id", + "columns": [ + { + "expression": "blocked_by_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_openrouter_upstream_safety_identifier": { + "name": "UQ_kilocode_users_openrouter_upstream_safety_identifier", + "columns": [ + { + "expression": "openrouter_upstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"openrouter_upstream_safety_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_openrouter_downstream_safety_identifier": { + "name": "UQ_kilocode_users_openrouter_downstream_safety_identifier", + "columns": [ + { + "expression": "openrouter_downstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"openrouter_downstream_safety_identifier\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_kilocode_users_vercel_downstream_safety_identifier": { + "name": "UQ_kilocode_users_vercel_downstream_safety_identifier", + "columns": [ + { + "expression": "vercel_downstream_safety_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"kilocode_users\".\"vercel_downstream_safety_identifier\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_normalized_email": { + "name": "IDX_kilocode_users_normalized_email", + "columns": [ + { + "expression": "normalized_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_kilocode_users_email_domain": { + "name": "IDX_kilocode_users_email_domain", + "columns": [ + { + "expression": "email_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "kilocode_users_app_store_account_token_unique": { + "name": "kilocode_users_app_store_account_token_unique", + "nullsNotDistinct": false, + "columns": [ + "app_store_account_token" + ] + }, + "UQ_b1afacbcf43f2c7c4cb9f7e7faa": { + "name": "UQ_b1afacbcf43f2c7c4cb9f7e7faa", + "nullsNotDistinct": false, + "columns": [ + "google_user_email" + ] + } + }, + "policies": {}, + "checkConstraints": { + "blocked_reason_not_empty": { + "name": "blocked_reason_not_empty", + "value": "length(blocked_reason) > 0" + }, + "kilocode_users_is_super_admin_requires_admin_check": { + "name": "kilocode_users_is_super_admin_requires_admin_check", + "value": "NOT \"kilocode_users\".\"is_super_admin\" OR \"kilocode_users\".\"is_admin\"" + }, + "kilocode_users_can_view_sessions_requires_admin_check": { + "name": "kilocode_users_can_view_sessions_requires_admin_check", + "value": "NOT \"kilocode_users\".\"can_view_sessions\" OR \"kilocode_users\".\"is_admin\"" + }, + "kilocode_users_can_manage_credits_requires_admin_check": { + "name": "kilocode_users_can_manage_credits_requires_admin_check", + "value": "NOT \"kilocode_users\".\"can_manage_credits\" OR \"kilocode_users\".\"is_admin\"" + } + }, + "isRLSEnabled": false + }, + "public.magic_link_tokens": { + "name": "magic_link_tokens", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_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()" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reserved_until": { + "name": "reserved_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'magic_link'" + }, + "challenge_id": { + "name": "challenge_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_magic_link_tokens_email": { + "name": "idx_magic_link_tokens_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_magic_link_tokens_expires_at": { + "name": "idx_magic_link_tokens_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_magic_link_tokens_challenge_id": { + "name": "UQ_magic_link_tokens_challenge_id", + "columns": [ + { + "expression": "challenge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"magic_link_tokens\".\"challenge_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_expires_at_future": { + "name": "check_expires_at_future", + "value": "\"magic_link_tokens\".\"expires_at\" > \"magic_link_tokens\".\"created_at\"" + }, + "check_magic_link_tokens_purpose": { + "name": "check_magic_link_tokens_purpose", + "value": "\"magic_link_tokens\".\"purpose\" IN ('magic_link', 'sign_in_code', 'data_export_download')" + } + }, + "isRLSEnabled": false + }, + "public.microdollar_usage": { + "name": "microdollar_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_hit_tokens": { + "name": "cache_hit_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_model": { + "name": "requested_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_discount": { + "name": "cache_discount", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "abuse_classification": { + "name": "abuse_classification", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inference_provider": { + "name": "inference_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_created_at": { + "name": "idx_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_created_at_rollup": { + "name": "idx_microdollar_usage_created_at_rollup", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "idx_abuse_classification": { + "name": "idx_abuse_classification", + "columns": [ + { + "expression": "abuse_classification", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_kilo_user_id_created_at2": { + "name": "idx_kilo_user_id_created_at2", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_organization_id": { + "name": "idx_microdollar_usage_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"microdollar_usage\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microdollar_usage_daily": { + "name": "microdollar_usage_daily", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_microdollar_usage_daily_personal": { + "name": "idx_microdollar_usage_daily_personal", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"microdollar_usage_daily\".\"organization_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_daily_org": { + "name": "idx_microdollar_usage_daily_org", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"microdollar_usage_daily\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.microdollar_usage_daily_repairs": { + "name": "microdollar_usage_daily_repairs", + "schema": "", + "columns": { + "usage_id": { + "name": "usage_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_token": { + "name": "claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error_redacted": { + "name": "last_error_redacted", + "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": { + "IDX_microdollar_usage_daily_repairs_claim": { + "name": "IDX_microdollar_usage_daily_repairs_claim", + "columns": [ + { + "expression": "attempt_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "microdollar_usage_daily_repairs_usage_id_microdollar_usage_id_fk": { + "name": "microdollar_usage_daily_repairs_usage_id_microdollar_usage_id_fk", + "tableFrom": "microdollar_usage_daily_repairs", + "tableTo": "microdollar_usage", + "columnsFrom": [ + "usage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "microdollar_usage_daily_repairs_attempt_count_check": { + "name": "microdollar_usage_daily_repairs_attempt_count_check", + "value": "\"microdollar_usage_daily_repairs\".\"attempt_count\" >= 0" + }, + "microdollar_usage_daily_repairs_claim_token_check": { + "name": "microdollar_usage_daily_repairs_claim_token_check", + "value": "(\"microdollar_usage_daily_repairs\".\"claimed_at\" IS NULL AND \"microdollar_usage_daily_repairs\".\"claim_token\" IS NULL) OR (\"microdollar_usage_daily_repairs\".\"claimed_at\" IS NOT NULL AND \"microdollar_usage_daily_repairs\".\"claim_token\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.microdollar_usage_metadata": { + "name": "microdollar_usage_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "http_user_agent_id": { + "name": "http_user_agent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "http_ip_id": { + "name": "http_ip_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_city_id": { + "name": "vercel_ip_city_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_country_id": { + "name": "vercel_ip_country_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_latitude": { + "name": "vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "vercel_ip_longitude": { + "name": "vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "ja4_digest_id": { + "name": "ja4_digest_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_prompt_prefix": { + "name": "user_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_prefix_id": { + "name": "system_prompt_prefix_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "system_prompt_length": { + "name": "system_prompt_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_middle_out_transform": { + "name": "has_middle_out_transform", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "upstream_id": { + "name": "upstream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finish_reason_id": { + "name": "finish_reason_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency": { + "name": "latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "moderation_latency": { + "name": "moderation_latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "generation_time": { + "name": "generation_time", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_byok": { + "name": "is_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_user_byok": { + "name": "is_user_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "streamed": { + "name": "streamed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancelled": { + "name": "cancelled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "editor_name_id": { + "name": "editor_name_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "api_kind_id": { + "name": "api_kind_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "has_tools": { + "name": "has_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode_id": { + "name": "mode_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_model_id": { + "name": "auto_model_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reasoning_setting_id": { + "name": "reasoning_setting_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "market_cost": { + "name": "market_cost", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "abuse_delay": { + "name": "abuse_delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "abuse_downgraded_from": { + "name": "abuse_downgraded_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_microdollar_usage_metadata_created_at": { + "name": "idx_microdollar_usage_metadata_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_microdollar_usage_metadata_session_id": { + "name": "idx_microdollar_usage_metadata_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"microdollar_usage_metadata\".\"session_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "microdollar_usage_metadata_http_user_agent_id_http_user_agent_http_user_agent_id_fk": { + "name": "microdollar_usage_metadata_http_user_agent_id_http_user_agent_http_user_agent_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "http_user_agent", + "columnsFrom": [ + "http_user_agent_id" + ], + "columnsTo": [ + "http_user_agent_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_http_ip_id_http_ip_http_ip_id_fk": { + "name": "microdollar_usage_metadata_http_ip_id_http_ip_http_ip_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "http_ip", + "columnsFrom": [ + "http_ip_id" + ], + "columnsTo": [ + "http_ip_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_vercel_ip_city_id_vercel_ip_city_vercel_ip_city_id_fk": { + "name": "microdollar_usage_metadata_vercel_ip_city_id_vercel_ip_city_vercel_ip_city_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "vercel_ip_city", + "columnsFrom": [ + "vercel_ip_city_id" + ], + "columnsTo": [ + "vercel_ip_city_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_vercel_ip_country_id_vercel_ip_country_vercel_ip_country_id_fk": { + "name": "microdollar_usage_metadata_vercel_ip_country_id_vercel_ip_country_vercel_ip_country_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "vercel_ip_country", + "columnsFrom": [ + "vercel_ip_country_id" + ], + "columnsTo": [ + "vercel_ip_country_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_ja4_digest_id_ja4_digest_ja4_digest_id_fk": { + "name": "microdollar_usage_metadata_ja4_digest_id_ja4_digest_ja4_digest_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "ja4_digest", + "columnsFrom": [ + "ja4_digest_id" + ], + "columnsTo": [ + "ja4_digest_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "microdollar_usage_metadata_system_prompt_prefix_id_system_prompt_prefix_system_prompt_prefix_id_fk": { + "name": "microdollar_usage_metadata_system_prompt_prefix_id_system_prompt_prefix_system_prompt_prefix_id_fk", + "tableFrom": "microdollar_usage_metadata", + "tableTo": "system_prompt_prefix", + "columnsFrom": [ + "system_prompt_prefix_id" + ], + "columnsTo": [ + "system_prompt_prefix_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mode": { + "name": "mode", + "schema": "", + "columns": { + "mode_id": { + "name": "mode_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_mode": { + "name": "UQ_mode", + "columns": [ + { + "expression": "mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_stats": { + "name": "model_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "is_featured": { + "name": "is_featured", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_stealth": { + "name": "is_stealth", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_recommended": { + "name": "is_recommended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "openrouter_id": { + "name": "openrouter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "aa_slug": { + "name": "aa_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_creator": { + "name": "model_creator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creator_slug": { + "name": "creator_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_date": { + "name": "release_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "price_input": { + "name": "price_input", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "price_output": { + "name": "price_output", + "type": "numeric(10, 6)", + "primaryKey": false, + "notNull": false + }, + "coding_index": { + "name": "coding_index", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "speed_tokens_per_sec": { + "name": "speed_tokens_per_sec", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "context_length": { + "name": "context_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_output_tokens": { + "name": "max_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "input_modalities": { + "name": "input_modalities", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "openrouter_data": { + "name": "openrouter_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "benchmarks": { + "name": "benchmarks", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "chart_data": { + "name": "chart_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_stats_openrouter_id": { + "name": "IDX_model_stats_openrouter_id", + "columns": [ + { + "expression": "openrouter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_slug": { + "name": "IDX_model_stats_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_is_active": { + "name": "IDX_model_stats_is_active", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_creator_slug": { + "name": "IDX_model_stats_creator_slug", + "columns": [ + { + "expression": "creator_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_price_input": { + "name": "IDX_model_stats_price_input", + "columns": [ + { + "expression": "price_input", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_coding_index": { + "name": "IDX_model_stats_coding_index", + "columns": [ + { + "expression": "coding_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_stats_context_length": { + "name": "IDX_model_stats_context_length", + "columns": [ + { + "expression": "context_length", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_stats_openrouter_id_unique": { + "name": "model_stats_openrouter_id_unique", + "nullsNotDistinct": false, + "columns": [ + "openrouter_id" + ] + }, + "model_stats_slug_unique": { + "name": "model_stats_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.model_eval_ingestions": { + "name": "model_eval_ingestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bench_eval_name": { + "name": "bench_eval_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bench_eval_url": { + "name": "bench_eval_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_stats_id": { + "name": "model_stats_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_source": { + "name": "task_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "n_total_trials": { + "name": "n_total_trials", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "n_attempts": { + "name": "n_attempts", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_score": { + "name": "total_score", + "type": "numeric(14, 6)", + "primaryKey": false, + "notNull": true + }, + "overall_score": { + "name": "overall_score", + "type": "numeric(12, 8)", + "primaryKey": false, + "notNull": true + }, + "n_errored": { + "name": "n_errored", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "avg_cost_microdollars": { + "name": "avg_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "total_cost_microdollars": { + "name": "total_cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_input_tokens": { + "name": "avg_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_output_tokens": { + "name": "avg_output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_cache_read_tokens": { + "name": "avg_cache_read_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_cache_read_tokens": { + "name": "total_cache_read_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "avg_execution_ms": { + "name": "avg_execution_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "promoted_at": { + "name": "promoted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "promoted_by_email": { + "name": "promoted_by_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "promotion_note": { + "name": "promotion_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_model_eval_ingestions_lookup": { + "name": "IDX_model_eval_ingestions_lookup", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "model", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "variant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "promoted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_eval_ingestions_model_stats": { + "name": "IDX_model_eval_ingestions_model_stats", + "columns": [ + { + "expression": "model_stats_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_model_eval_ingestions_promoted_by_email_lower": { + "name": "IDX_model_eval_ingestions_promoted_by_email_lower", + "columns": [ + { + "expression": "LOWER(\"promoted_by_email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "model_eval_ingestions_model_stats_id_model_stats_id_fk": { + "name": "model_eval_ingestions_model_stats_id_model_stats_id_fk", + "tableFrom": "model_eval_ingestions", + "tableTo": "model_stats", + "columnsFrom": [ + "model_stats_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "model_eval_ingestions_bench_eval_name_unique": { + "name": "model_eval_ingestions_bench_eval_name_unique", + "nullsNotDistinct": false, + "columns": [ + "bench_eval_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.models_by_provider": { + "name": "models_by_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "openrouter": { + "name": "openrouter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "vercel": { + "name": "vercel", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.native_admission_challenges": { + "name": "native_admission_challenges", + "schema": "", + "columns": { + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_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": { + "IDX_native_admission_challenges_expires_at": { + "name": "IDX_native_admission_challenges_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.native_attested_keys": { + "name": "native_attested_keys", + "schema": "", + "columns": { + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sign_count": { + "name": "sign_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attested_at": { + "name": "attested_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": { + "IDX_native_attested_keys_kilo_user_id": { + "name": "IDX_native_attested_keys_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "native_attested_keys_kilo_user_id_kilocode_users_id_fk": { + "name": "native_attested_keys_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "native_attested_keys", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "native_attested_keys_platform_check": { + "name": "native_attested_keys_platform_check", + "value": "\"native_attested_keys\".\"platform\" IN ('ios', 'android')" + } + }, + "isRLSEnabled": false + }, + "public.openai_chatgpt_connections": { + "name": "openai_chatgpt_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "encrypted_connection": { + "name": "encrypted_connection", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_shared_services": { + "name": "is_shared_services", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "usage_limit_reached_at": { + "name": "usage_limit_reached_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "usage_limit_resets_at": { + "name": "usage_limit_resets_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "usage_limit_error_code": { + "name": "usage_limit_error_code", + "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()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_openai_chatgpt_connections_personal": { + "name": "UQ_openai_chatgpt_connections_personal", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"openai_chatgpt_connections\".\"organization_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_openai_chatgpt_connections_org_member": { + "name": "UQ_openai_chatgpt_connections_org_member", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"openai_chatgpt_connections\".\"organization_id\" IS NOT NULL AND \"openai_chatgpt_connections\".\"is_shared_services\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_openai_chatgpt_connections_org_shared_services": { + "name": "UQ_openai_chatgpt_connections_org_shared_services", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"openai_chatgpt_connections\".\"is_shared_services\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_openai_chatgpt_connections_organization_id": { + "name": "IDX_openai_chatgpt_connections_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "openai_chatgpt_connections_kilo_user_id_kilocode_users_id_fk": { + "name": "openai_chatgpt_connections_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "openai_chatgpt_connections", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "openai_chatgpt_connections_organization_id_organizations_id_fk": { + "name": "openai_chatgpt_connections_organization_id_organizations_id_fk", + "tableFrom": "openai_chatgpt_connections", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.operation_ledgers": { + "name": "operation_ledgers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "operation_key": { + "name": "operation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "taxonomy": { + "name": "taxonomy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admitted'" + }, + "outcome_code": { + "name": "outcome_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canonical_result": { + "name": "canonical_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "settled_at": { + "name": "settled_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": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_operation_ledgers_kilo_user_id_domain_operation_key": { + "name": "UQ_operation_ledgers_kilo_user_id_domain_operation_key", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "operation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_operation_ledgers_status_expires_at": { + "name": "IDX_operation_ledgers_status_expires_at", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_operation_ledgers_provider_ref": { + "name": "IDX_operation_ledgers_provider_ref", + "columns": [ + { + "expression": "provider_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"operation_ledgers\".\"provider_ref\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_audit_logs": { + "name": "organization_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_audit_logs_organization_id": { + "name": "IDX_organization_audit_logs_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_action": { + "name": "IDX_organization_audit_logs_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_actor_id": { + "name": "IDX_organization_audit_logs_actor_id", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_audit_logs_created_at": { + "name": "IDX_organization_audit_logs_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_group_memberships": { + "name": "organization_group_memberships", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by_kilo_user_id": { + "name": "assigned_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_group_memberships_organization_user": { + "name": "IDX_organization_group_memberships_organization_user", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "FK_organization_group_memberships_group": { + "name": "FK_organization_group_memberships_group", + "tableFrom": "organization_group_memberships", + "tableTo": "organization_groups", + "columnsFrom": [ + "organization_id", + "group_id" + ], + "columnsTo": [ + "organization_id", + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "FK_organization_group_memberships_member": { + "name": "FK_organization_group_memberships_member", + "tableFrom": "organization_group_memberships", + "tableTo": "organization_memberships", + "columnsFrom": [ + "organization_id", + "kilo_user_id" + ], + "columnsTo": [ + "organization_id", + "kilo_user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "PK_organization_group_memberships": { + "name": "PK_organization_group_memberships", + "columns": [ + "organization_id", + "group_id", + "kilo_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_group_policy_settings": { + "name": "organization_group_policy_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "default_policies": { + "name": "default_policies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[{\"type\":\"model_access\",\"data\":{\"mode\":\"all\"}}]'::jsonb" + }, + "policy_revision": { + "name": "policy_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "updated_by_kilo_user_id": { + "name": "updated_by_kilo_user_id", + "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": {}, + "foreignKeys": { + "organization_group_policy_settings_organization_id_organizations_id_fk": { + "name": "organization_group_policy_settings_organization_id_organizations_id_fk", + "tableFrom": "organization_group_policy_settings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_group_policy_settings_revision_check": { + "name": "organization_group_policy_settings_revision_check", + "value": "\"organization_group_policy_settings\".\"policy_revision\" >= 1" + } + }, + "isRLSEnabled": false + }, + "public.organization_groups": { + "name": "organization_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policies": { + "name": "policies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "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": { + "UQ_organization_groups_organization_id_canonical_name": { + "name": "UQ_organization_groups_organization_id_canonical_name", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(btrim(\"name\"))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_groups_organization_id": { + "name": "IDX_organization_groups_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_groups_organization_id_organizations_id_fk": { + "name": "organization_groups_organization_id_organizations_id_fk", + "tableFrom": "organization_groups", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_groups_organization_id_id": { + "name": "UQ_organization_groups_organization_id_id", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_groups_name_check": { + "name": "organization_groups_name_check", + "value": "char_length(btrim(\"organization_groups\".\"name\")) BETWEEN 1 AND 80" + }, + "organization_groups_description_check": { + "name": "organization_groups_description_check", + "value": "\"organization_groups\".\"description\" IS NULL OR char_length(\"organization_groups\".\"description\") <= 500" + } + }, + "isRLSEnabled": false + }, + "public.organization_invitations": { + "name": "organization_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "authentication_requirement": { + "name": "authentication_requirement", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "sso_source_organization_id": { + "name": "sso_source_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_organization_invitations_token": { + "name": "UQ_organization_invitations_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_org_id": { + "name": "IDX_organization_invitations_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_email": { + "name": "IDX_organization_invitations_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_invitations_expires_at": { + "name": "IDX_organization_invitations_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_invitations_sso_source_organization_id_organizations_id_fk": { + "name": "organization_invitations_sso_source_organization_id_organizations_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "organizations", + "columnsFrom": [ + "sso_source_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_membership_removals": { + "name": "organization_membership_removals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "removed_by": { + "name": "removed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_role": { + "name": "previous_role", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "IDX_org_membership_removals_org_id": { + "name": "IDX_org_membership_removals_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_org_membership_removals_user_id": { + "name": "IDX_org_membership_removals_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_org_membership_removals_org_user": { + "name": "UQ_org_membership_removals_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_memberships": { + "name": "organization_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_organization_memberships_org_id": { + "name": "IDX_organization_memberships_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_memberships_user_id": { + "name": "IDX_organization_memberships_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_memberships_org_user": { + "name": "UQ_organization_memberships_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_recommendation_dismissals": { + "name": "organization_recommendation_dismissals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recommendation_key": { + "name": "recommendation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_by_user_id": { + "name": "dismissed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_recommendation_dismissals_owned_by_organization_id_organizations_id_fk": { + "name": "organization_recommendation_dismissals_owned_by_organization_id_organizations_id_fk", + "tableFrom": "organization_recommendation_dismissals", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_recommendation_dismissals_dismissed_by_user_id_kilocode_users_id_fk": { + "name": "organization_recommendation_dismissals_dismissed_by_user_id_kilocode_users_id_fk", + "tableFrom": "organization_recommendation_dismissals", + "tableTo": "kilocode_users", + "columnsFrom": [ + "dismissed_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_org_recommendation_dismissals_org_key": { + "name": "UQ_org_recommendation_dismissals_org_key", + "nullsNotDistinct": false, + "columns": [ + "owned_by_organization_id", + "recommendation_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_seats_purchases": { + "name": "organization_seats_purchases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subscription_stripe_id": { + "name": "subscription_stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "seat_count": { + "name": "seat_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "subscription_status": { + "name": "subscription_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "billing_cycle": { + "name": "billing_cycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'monthly'" + } + }, + "indexes": { + "IDX_organization_seats_org_id": { + "name": "IDX_organization_seats_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_expires_at": { + "name": "IDX_organization_seats_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_created_at": { + "name": "IDX_organization_seats_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_updated_at": { + "name": "IDX_organization_seats_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_seats_starts_at": { + "name": "IDX_organization_seats_starts_at", + "columns": [ + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_seats_idempotency_key": { + "name": "UQ_organization_seats_idempotency_key", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_service_fee_exemptions": { + "name": "organization_service_fee_exemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_exempt": { + "name": "is_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "changed_by_kilo_user_id": { + "name": "changed_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_org_service_fee_exemptions_org_created_at": { + "name": "IDX_org_service_fee_exemptions_org_created_at", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "FK_org_svc_fee_exemptions_organization": { + "name": "FK_org_svc_fee_exemptions_organization", + "tableFrom": "organization_service_fee_exemptions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "FK_org_svc_fee_exemptions_changed_by": { + "name": "FK_org_svc_fee_exemptions_changed_by", + "tableFrom": "organization_service_fee_exemptions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "changed_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_service_fee_exemptions_reason_check": { + "name": "organization_service_fee_exemptions_reason_check", + "value": "length(trim(\"organization_service_fee_exemptions\".\"reason\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.organization_user_limits": { + "name": "organization_user_limits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limit_type": { + "name": "limit_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "microdollar_limit": { + "name": "microdollar_limit", + "type": "bigint", + "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": { + "IDX_organization_user_limits_org_id": { + "name": "IDX_organization_user_limits_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_user_limits_user_id": { + "name": "IDX_organization_user_limits_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_user_limits_org_user": { + "name": "UQ_organization_user_limits_org_user", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id", + "limit_type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_user_usage": { + "name": "organization_user_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "limit_type": { + "name": "limit_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "microdollar_usage": { + "name": "microdollar_usage", + "type": "bigint", + "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": { + "IDX_organization_user_daily_usage_org_id": { + "name": "IDX_organization_user_daily_usage_org_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organization_user_daily_usage_user_id": { + "name": "IDX_organization_user_daily_usage_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_user_daily_usage_org_user_date": { + "name": "UQ_organization_user_daily_usage_org_user_date", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "kilo_user_id", + "limit_type", + "usage_date" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_vercel_compute_credentials": { + "name": "organization_vercel_compute_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_encrypted": { + "name": "token_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "token_scope": { + "name": "token_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'team'" + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_slug": { + "name": "team_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_slug": { + "name": "project_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "setup_status": { + "name": "setup_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "setup_step": { + "name": "setup_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "setup_error": { + "name": "setup_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_generation": { + "name": "build_generation", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "runtime_build_id": { + "name": "runtime_build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_snapshot_id": { + "name": "runtime_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_wrapper_version": { + "name": "runtime_wrapper_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_released_at": { + "name": "runtime_released_at", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "runtime_digest": { + "name": "runtime_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upgrade_status": { + "name": "upgrade_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upgrade_step": { + "name": "upgrade_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "upgrade_error": { + "name": "upgrade_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "setup_started_at": { + "name": "setup_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "setup_completed_at": { + "name": "setup_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": { + "UQ_organization_vercel_compute_credentials_organization": { + "name": "UQ_organization_vercel_compute_credentials_organization", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_vercel_compute_credentials\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_organization_vercel_compute_credentials_user": { + "name": "UQ_organization_vercel_compute_credentials_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_vercel_compute_credentials\".\"user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_vercel_compute_credentials_organization_id_organizations_id_fk": { + "name": "organization_vercel_compute_credentials_organization_id_organizations_id_fk", + "tableFrom": "organization_vercel_compute_credentials", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_vercel_compute_credentials_owner_check": { + "name": "organization_vercel_compute_credentials_owner_check", + "value": "(\"organization_vercel_compute_credentials\".\"organization_id\" IS NOT NULL AND \"organization_vercel_compute_credentials\".\"user_id\" IS NULL) OR (\"organization_vercel_compute_credentials\".\"organization_id\" IS NULL AND \"organization_vercel_compute_credentials\".\"user_id\" IS NOT NULL)" + }, + "organization_vercel_compute_credentials_ids_non_empty": { + "name": "organization_vercel_compute_credentials_ids_non_empty", + "value": "length(trim(\"organization_vercel_compute_credentials\".\"team_id\")) > 0 AND length(trim(\"organization_vercel_compute_credentials\".\"project_id\")) > 0" + }, + "organization_vercel_compute_credentials_token_scope_check": { + "name": "organization_vercel_compute_credentials_token_scope_check", + "value": "\"organization_vercel_compute_credentials\".\"token_scope\" IN ('team', 'project')" + }, + "organization_vercel_compute_credentials_status_check": { + "name": "organization_vercel_compute_credentials_status_check", + "value": "\"organization_vercel_compute_credentials\".\"setup_status\" IN ('pending', 'building', 'ready', 'failed')" + }, + "organization_vercel_compute_credentials_ready_fields_check": { + "name": "organization_vercel_compute_credentials_ready_fields_check", + "value": "\"organization_vercel_compute_credentials\".\"setup_status\" <> 'ready' OR (\"organization_vercel_compute_credentials\".\"runtime_snapshot_id\" IS NOT NULL AND \"organization_vercel_compute_credentials\".\"project_slug\" IS NOT NULL AND \"organization_vercel_compute_credentials\".\"setup_completed_at\" IS NOT NULL)" + }, + "organization_vercel_compute_credentials_upgrade_status_check": { + "name": "organization_vercel_compute_credentials_upgrade_status_check", + "value": "\"organization_vercel_compute_credentials\".\"upgrade_status\" IS NULL OR \"organization_vercel_compute_credentials\".\"upgrade_status\" IN ('pending', 'building', 'failed')" + } + }, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "name": { + "name": "name", + "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()" + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "microdollars_balance": { + "name": "microdollars_balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_microdollars_acquired": { + "name": "total_microdollars_acquired", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "next_credit_expiration_at": { + "name": "next_credit_expiration_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_top_up_enabled": { + "name": "auto_top_up_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "seat_count": { + "name": "seat_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "require_seats": { + "name": "require_seats", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_kilo_user_id": { + "name": "created_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sso_domain": { + "name": "sso_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_organization_id": { + "name": "parent_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'teams'" + }, + "free_trial_end_at": { + "name": "free_trial_end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "company_domain": { + "name": "company_domain", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_organizations_sso_domain": { + "name": "IDX_organizations_sso_domain", + "columns": [ + { + "expression": "sso_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_organizations_parent_organization_id": { + "name": "IDX_organizations_parent_organization_id", + "columns": [ + { + "expression": "parent_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_organizations_live_sales_demo_per_owner": { + "name": "UQ_organizations_live_sales_demo_per_owner", + "columns": [ + { + "expression": "created_by_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "(\"organizations\".\"settings\"->>'is_sales_demo')::boolean = true AND \"organizations\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organizations_parent_organization_id_organizations_id_fk": { + "name": "organizations_parent_organization_id_organizations_id_fk", + "tableFrom": "organizations", + "tableTo": "organizations", + "columnsFrom": [ + "parent_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organizations_name_not_empty_check": { + "name": "organizations_name_not_empty_check", + "value": "length(trim(\"organizations\".\"name\")) > 0" + }, + "organizations_not_parented_by_self_check": { + "name": "organizations_not_parented_by_self_check", + "value": "\"organizations\".\"parent_organization_id\" IS NULL OR \"organizations\".\"parent_organization_id\" <> \"organizations\".\"id\"" + } + }, + "isRLSEnabled": false + }, + "public.organization_modes": { + "name": "organization_modes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_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 + }, + "created_by": { + "name": "created_by", + "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()" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "IDX_organization_modes_organization_id": { + "name": "IDX_organization_modes_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_organization_modes_org_id_slug": { + "name": "UQ_organization_modes_org_id_slug", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey_challenges": { + "name": "passkey_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_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": { + "idx_passkey_challenges_expires_at": { + "name": "idx_passkey_challenges_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_passkey_challenges_kilo_user_id": { + "name": "idx_passkey_challenges_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_passkey_challenges_kind": { + "name": "check_passkey_challenges_kind", + "value": "\"passkey_challenges\".\"kind\" IN ('registration', 'authentication')" + } + }, + "isRLSEnabled": false + }, + "public.passkey_credentials": { + "name": "passkey_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sign_count": { + "name": "sign_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_passkey_credentials_credential_id": { + "name": "UQ_passkey_credentials_credential_id", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_passkey_credentials_kilo_user_id": { + "name": "idx_passkey_credentials_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey_sign_in_tickets": { + "name": "passkey_sign_in_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "ticket_hash": { + "name": "ticket_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_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": { + "UQ_passkey_sign_in_tickets_ticket_hash": { + "name": "UQ_passkey_sign_in_tickets_ticket_hash", + "columns": [ + { + "expression": "ticket_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_passkey_sign_in_tickets_expires_at": { + "name": "idx_passkey_sign_in_tickets_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_passkey_sign_in_tickets_kilo_user_id": { + "name": "idx_passkey_sign_in_tickets_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_methods": { + "name": "payment_methods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_fingerprint": { + "name": "stripe_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last4": { + "name": "last4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line1": { + "name": "address_line1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line2": { + "name": "address_line2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_city": { + "name": "address_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_state": { + "name": "address_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_zip": { + "name": "address_zip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_country": { + "name": "address_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "three_d_secure_supported": { + "name": "three_d_secure_supported", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "funding": { + "name": "funding", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "regulated_status": { + "name": "regulated_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line1_check_status": { + "name": "address_line1_check_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postal_code_check_status": { + "name": "postal_code_check_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "eligible_for_free_credits": { + "name": "eligible_for_free_credits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_data": { + "name": "stripe_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "IDX_d7d7fb15569674aaadcfbc0428": { + "name": "IDX_d7d7fb15569674aaadcfbc0428", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_e1feb919d0ab8a36381d5d5138": { + "name": "IDX_e1feb919d0ab8a36381d5d5138", + "columns": [ + { + "expression": "stripe_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_payment_methods_organization_id": { + "name": "IDX_payment_methods_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_29df1b0403df5792c96bbbfdbe6": { + "name": "UQ_29df1b0403df5792c96bbbfdbe6", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "stripe_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_impact_sale_reversals": { + "name": "pending_impact_sale_reversals", + "schema": "", + "columns": { + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "dispute_id": { + "name": "dispute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_date": { + "name": "event_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_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()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pending_impact_sale_reversals_attempt_count_non_negative_check": { + "name": "pending_impact_sale_reversals_attempt_count_non_negative_check", + "value": "\"pending_impact_sale_reversals\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.platform_access_token_credentials": { + "name": "platform_access_token_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_encrypted": { + "name": "token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider_credential_type": { + "name": "provider_credential_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_resource_id": { + "name": "provider_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_base_url": { + "name": "provider_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "provider_scopes": { + "name": "provider_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_verified_at": { + "name": "provider_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_validated_at": { + "name": "last_validated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_access_token_credentials_integration_level": { + "name": "UQ_platform_access_token_credentials_integration_level", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_access_token_credentials\".\"provider_resource_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_access_token_credentials_resource": { + "name": "UQ_platform_access_token_credentials_resource", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_credential_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_access_token_credentials\".\"provider_resource_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_access_token_credentials_authorized_by_user_id": { + "name": "IDX_platform_access_token_credentials_authorized_by_user_id", + "columns": [ + { + "expression": "authorized_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_access_token_credentials_authorized_by_user_id_kilocode_users_id_fk": { + "name": "platform_access_token_credentials_authorized_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_access_token_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "authorized_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "FK_platform_access_token_credentials_parent": { + "name": "FK_platform_access_token_credentials_parent", + "tableFrom": "platform_access_token_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_access_token_credentials_credential_version_check": { + "name": "platform_access_token_credentials_credential_version_check", + "value": "\"platform_access_token_credentials\".\"credential_version\" > 0" + }, + "platform_access_token_credentials_resource_id_check": { + "name": "platform_access_token_credentials_resource_id_check", + "value": "\"platform_access_token_credentials\".\"provider_resource_id\" IS NULL OR \"platform_access_token_credentials\".\"provider_resource_id\" <> ''" + } + }, + "isRLSEnabled": false + }, + "public.platform_integrations": { + "name": "platform_integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration_type": { + "name": "integration_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform_installation_id": { + "name": "platform_installation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_account_id": { + "name": "platform_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_account_login": { + "name": "platform_account_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "repository_access": { + "name": "repository_access", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repositories": { + "name": "repositories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "repositories_synced_at": { + "name": "repositories_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_at": { + "name": "auth_invalid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_invalid_reason": { + "name": "auth_invalid_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "kilo_requester_user_id": { + "name": "kilo_requester_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_requester_account_id": { + "name": "platform_requester_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "integration_status": { + "name": "integration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "suspended_by": { + "name": "suspended_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'standard'" + }, + "github_installation_id": { + "name": "github_installation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "github_disconnected_at": { + "name": "github_disconnected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "github_connection_role": { + "name": "github_connection_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_authorized_by_user_id": { + "name": "github_authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_authorized_user_id": { + "name": "github_authorized_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_authorized_at": { + "name": "github_authorized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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": { + "UQ_platform_integrations_owned_by_org_platform_inst": { + "name": "UQ_platform_integrations_owned_by_org_platform_inst", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_owned_by_user_platform_inst": { + "name": "UQ_platform_integrations_owned_by_user_platform_inst", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_slack_platform_inst": { + "name": "UQ_platform_integrations_slack_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'slack' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_linear_platform_inst": { + "name": "UQ_platform_integrations_linear_platform_inst", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'linear' AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_github_org_canonical": { + "name": "UQ_platform_integrations_github_org_canonical", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'github' AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL AND \"platform_integrations\".\"github_installation_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_github_workflow_canonical": { + "name": "UQ_platform_integrations_github_workflow_canonical", + "columns": [ + { + "expression": "github_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"github_connection_role\" = 'workflow'", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_github_workflow_identity": { + "name": "UQ_platform_integrations_github_workflow_identity", + "columns": [ + { + "expression": "COALESCE(\"github_app_type\", 'standard')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"github_connection_role\" = 'workflow'", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_github_user_canonical": { + "name": "UQ_platform_integrations_github_user_canonical", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'github' AND \"platform_integrations\".\"owned_by_user_id\" IS NOT NULL AND \"platform_integrations\".\"github_installation_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_github_org_pending_target": { + "name": "UQ_platform_integrations_github_org_pending_target", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'github' AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL AND \"platform_integrations\".\"integration_status\" = 'pending' AND \"platform_integrations\".\"platform_installation_id\" IS NULL AND \"platform_integrations\".\"platform_account_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_github_user_pending_target": { + "name": "UQ_platform_integrations_github_user_pending_target", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'github' AND \"platform_integrations\".\"owned_by_user_id\" IS NOT NULL AND \"platform_integrations\".\"integration_status\" = 'pending' AND \"platform_integrations\".\"platform_installation_id\" IS NULL AND \"platform_integrations\".\"platform_account_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_user_bitbucket": { + "name": "UQ_platform_integrations_user_bitbucket", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'bitbucket' AND \"platform_integrations\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_platform_integrations_org_bitbucket": { + "name": "UQ_platform_integrations_org_bitbucket", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"platform_integrations\".\"platform\" = 'bitbucket' AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_org_id": { + "name": "IDX_platform_integrations_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_user_id": { + "name": "IDX_platform_integrations_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform_inst_id": { + "name": "IDX_platform_integrations_platform_inst_id", + "columns": [ + { + "expression": "platform_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform": { + "name": "IDX_platform_integrations_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_org_platform": { + "name": "IDX_platform_integrations_owned_by_org_platform", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_owned_by_user_platform": { + "name": "IDX_platform_integrations_owned_by_user_platform", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_integration_status": { + "name": "IDX_platform_integrations_integration_status", + "columns": [ + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_kilo_requester": { + "name": "IDX_platform_integrations_kilo_requester", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_requester_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_integrations_platform_requester": { + "name": "IDX_platform_integrations_platform_requester", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "platform_requester_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_integrations_owned_by_organization_id_organizations_id_fk": { + "name": "platform_integrations_owned_by_organization_id_organizations_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_integrations_owned_by_user_id_kilocode_users_id_fk": { + "name": "platform_integrations_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_integrations_github_installation_id_github_app_installations_id_fk": { + "name": "platform_integrations_github_installation_id_github_app_installations_id_fk", + "tableFrom": "platform_integrations", + "tableTo": "github_app_installations", + "columnsFrom": [ + "github_installation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_integrations_github_connection_role_check": { + "name": "platform_integrations_github_connection_role_check", + "value": "\"platform_integrations\".\"github_connection_role\" IS NULL OR (\n \"platform_integrations\".\"platform\" = 'github' AND \"platform_integrations\".\"integration_type\" = 'app'\n AND \"platform_integrations\".\"platform_installation_id\" IS NOT NULL\n AND \"platform_integrations\".\"github_connection_role\" IN ('workflow', 'agent_only')\n AND (\"platform_integrations\".\"github_connection_role\" <> 'agent_only' OR \"platform_integrations\".\"github_installation_id\" IS NOT NULL)\n )" + }, + "platform_integrations_owner_check": { + "name": "platform_integrations_owner_check", + "value": "(\n (\"platform_integrations\".\"owned_by_user_id\" IS NOT NULL AND \"platform_integrations\".\"owned_by_organization_id\" IS NULL) OR\n (\"platform_integrations\".\"owned_by_user_id\" IS NULL AND \"platform_integrations\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.platform_oauth_credentials": { + "name": "platform_oauth_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subject_login": { + "name": "provider_subject_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_base_url": { + "name": "provider_base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret_encrypted": { + "name": "oauth_client_secret_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_platform_oauth_credentials_platform_integration_id": { + "name": "UQ_platform_oauth_credentials_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_platform_oauth_credentials_authorized_by_user_id": { + "name": "IDX_platform_oauth_credentials_authorized_by_user_id", + "columns": [ + { + "expression": "authorized_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk": { + "name": "platform_oauth_credentials_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "platform_oauth_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk": { + "name": "platform_oauth_credentials_authorized_by_user_id_kilocode_users_id_fk", + "tableFrom": "platform_oauth_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "authorized_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_oauth_credentials_credential_version_check": { + "name": "platform_oauth_credentials_credential_version_check", + "value": "\"platform_oauth_credentials\".\"credential_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.provider_oauth_attempts": { + "name": "provider_oauth_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'provider_install'" + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initiated_by_user_id": { + "name": "initiated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "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 + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_provider_oauth_attempts_user_pending": { + "name": "UQ_provider_oauth_attempts_user_pending", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"provider_oauth_attempts\".\"status\" = 'pending' AND \"provider_oauth_attempts\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_provider_oauth_attempts_org_pending": { + "name": "UQ_provider_oauth_attempts_org_pending", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"provider_oauth_attempts\".\"status\" = 'pending' AND \"provider_oauth_attempts\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_provider_oauth_attempts_expires_at": { + "name": "IDX_provider_oauth_attempts_expires_at", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_oauth_attempts_initiated_by_user_id_kilocode_users_id_fk": { + "name": "provider_oauth_attempts_initiated_by_user_id_kilocode_users_id_fk", + "tableFrom": "provider_oauth_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "initiated_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "provider_oauth_attempts_owned_by_user_id_kilocode_users_id_fk": { + "name": "provider_oauth_attempts_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "provider_oauth_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "provider_oauth_attempts_owned_by_organization_id_organizations_id_fk": { + "name": "provider_oauth_attempts_owned_by_organization_id_organizations_id_fk", + "tableFrom": "provider_oauth_attempts", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_oauth_attempts_state_hash_unique": { + "name": "provider_oauth_attempts_state_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "state_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "provider_oauth_attempts_provider_check": { + "name": "provider_oauth_attempts_provider_check", + "value": "\"provider_oauth_attempts\".\"provider\" IN ('slack', 'linear', 'discord')" + }, + "provider_oauth_attempts_status_check": { + "name": "provider_oauth_attempts_status_check", + "value": "\"provider_oauth_attempts\".\"status\" IN ('pending', 'consumed', 'expired')" + }, + "provider_oauth_attempts_purpose_check": { + "name": "provider_oauth_attempts_purpose_check", + "value": "\"provider_oauth_attempts\".\"purpose\" = 'provider_install'" + }, + "provider_oauth_attempts_owner_check": { + "name": "provider_oauth_attempts_owner_check", + "value": "num_nonnulls(\"provider_oauth_attempts\".\"owned_by_user_id\", \"provider_oauth_attempts\".\"owned_by_organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.reasoning_setting": { + "name": "reasoning_setting", + "schema": "", + "columns": { + "reasoning_setting_id": { + "name": "reasoning_setting_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "reasoning_setting": { + "name": "reasoning_setting", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_reasoning_setting": { + "name": "UQ_reasoning_setting", + "columns": [ + { + "expression": "reasoning_setting", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_code_usages": { + "name": "referral_code_usages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "referring_kilo_user_id": { + "name": "referring_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redeeming_kilo_user_id": { + "name": "redeeming_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_usd": { + "name": "amount_usd", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_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": { + "IDX_referral_code_usages_redeeming_kilo_user_id": { + "name": "IDX_referral_code_usages_redeeming_kilo_user_id", + "columns": [ + { + "expression": "redeeming_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_referral_code_usages_redeeming_user_id_code": { + "name": "UQ_referral_code_usages_redeeming_user_id_code", + "nullsNotDistinct": false, + "columns": [ + "redeeming_kilo_user_id", + "referring_kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_codes": { + "name": "referral_codes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "max_redemptions": { + "name": "max_redemptions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "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": { + "UQ_referral_codes_kilo_user_id": { + "name": "UQ_referral_codes_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_referral_codes_code": { + "name": "IDX_referral_codes_code", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sales_demo_spend_ledger": { + "name": "sales_demo_spend_ledger", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_kilo_user_id": { + "name": "owner_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "microdollars_used": { + "name": "microdollars_used", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sales_demo_spend_ledger_organization_id_organizations_id_fk": { + "name": "sales_demo_spend_ledger_organization_id_organizations_id_fk", + "tableFrom": "sales_demo_spend_ledger", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "sales_demo_spend_ledger_spend_positive": { + "name": "sales_demo_spend_ledger_spend_positive", + "value": "\"sales_demo_spend_ledger\".\"microdollars_used\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.security_advisor_check_catalog": { + "name": "security_advisor_check_catalog", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "explanation": { + "name": "explanation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "risk": { + "name": "risk", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_check_catalog_check_id_unique": { + "name": "security_advisor_check_catalog_check_id_unique", + "nullsNotDistinct": false, + "columns": [ + "check_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "security_advisor_check_catalog_severity_check": { + "name": "security_advisor_check_catalog_severity_check", + "value": "\"security_advisor_check_catalog\".\"severity\" in ('critical', 'warn', 'info')" + } + }, + "isRLSEnabled": false + }, + "public.security_advisor_content": { + "name": "security_advisor_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_content_key_unique": { + "name": "security_advisor_content_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_kiloclaw_coverage": { + "name": "security_advisor_kiloclaw_coverage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "area": { + "name": "area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "match_check_ids": { + "name": "match_check_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_advisor_kiloclaw_coverage_area_unique": { + "name": "security_advisor_kiloclaw_coverage_area_unique", + "nullsNotDistinct": false, + "columns": [ + "area" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_advisor_scans": { + "name": "security_advisor_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_platform": { + "name": "source_platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_method": { + "name": "source_method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_version": { + "name": "plugin_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "openclaw_version": { + "name": "openclaw_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_ip": { + "name": "public_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "findings_critical": { + "name": "findings_critical", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "findings_warn": { + "name": "findings_warn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "findings_info": { + "name": "findings_info", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_security_advisor_scans_user_created_at": { + "name": "idx_security_advisor_scans_user_created_at", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_advisor_scans_created_at": { + "name": "idx_security_advisor_scans_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_advisor_scans_platform": { + "name": "idx_security_advisor_scans_platform", + "columns": [ + { + "expression": "source_platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security_agent_commands": { + "name": "security_agent_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "operation_key": { + "name": "operation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'accepted'" + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_metadata": { + "name": "result_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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": { + "idx_security_agent_commands_org_created": { + "name": "idx_security_agent_commands_org_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_user_created": { + "name": "idx_security_agent_commands_user_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_status_updated": { + "name": "idx_security_agent_commands_status_updated", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_agent_commands_finding_created": { + "name": "idx_security_agent_commands_finding_created", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_agent_commands_org_operation_key": { + "name": "UQ_security_agent_commands_org_operation_key", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "operation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_commands\".\"owned_by_organization_id\" IS NOT NULL AND \"security_agent_commands\".\"operation_key\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "UQ_security_agent_commands_user_operation_key": { + "name": "UQ_security_agent_commands_user_operation_key", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "operation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_commands\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_commands\".\"operation_key\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_agent_commands_owned_by_organization_id_organizations_id_fk": { + "name": "security_agent_commands_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_commands_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_agent_commands_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_commands_finding_id_security_findings_id_fk": { + "name": "security_agent_commands_finding_id_security_findings_id_fk", + "tableFrom": "security_agent_commands", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_agent_commands_owner_check": { + "name": "security_agent_commands_owner_check", + "value": "(\n (\"security_agent_commands\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_commands\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_agent_commands\".\"owned_by_user_id\" IS NULL AND \"security_agent_commands\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_agent_commands_type_check": { + "name": "security_agent_commands_type_check", + "value": "\"security_agent_commands\".\"command_type\" IN ('sync', 'dismiss_finding', 'start_analysis', 'apply_auto_remediation')" + }, + "security_agent_commands_origin_check": { + "name": "security_agent_commands_origin_check", + "value": "\"security_agent_commands\".\"origin\" IN ('manual', 'dashboard_refresh', 'enable_initial_sync', 'settings_include_existing')" + }, + "security_agent_commands_status_check": { + "name": "security_agent_commands_status_check", + "value": "\"security_agent_commands\".\"status\" IN ('accepted', 'running', 'succeeded', 'failed', 'no_op')" + } + }, + "isRLSEnabled": false + }, + "public.security_agent_repository_sync_state": { + "name": "security_agent_repository_sync_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_succeeded_at": { + "name": "last_succeeded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_failure_code": { + "name": "last_failure_code", + "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": { + "UQ_security_agent_repository_sync_state_org_repo": { + "name": "UQ_security_agent_repository_sync_state_org_repo", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_repository_sync_state\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_agent_repository_sync_state_user_repo": { + "name": "UQ_security_agent_repository_sync_state_user_repo", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_agent_repository_sync_state\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_agent_repository_sync_state_owned_by_organization_id_organizations_id_fk": { + "name": "security_agent_repository_sync_state_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_agent_repository_sync_state", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_agent_repository_sync_state_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_agent_repository_sync_state_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_agent_repository_sync_state", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_agent_repository_sync_state_owner_check": { + "name": "security_agent_repository_sync_state_owner_check", + "value": "(\n (\"security_agent_repository_sync_state\".\"owned_by_user_id\" IS NOT NULL AND \"security_agent_repository_sync_state\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_agent_repository_sync_state\".\"owned_by_user_id\" IS NULL AND \"security_agent_repository_sync_state\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_analysis_owner_state": { + "name": "security_analysis_owner_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_analysis_enabled_at": { + "name": "auto_analysis_enabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "blocked_until": { + "name": "blocked_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "block_reason": { + "name": "block_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consecutive_actor_resolution_failures": { + "name": "consecutive_actor_resolution_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_actor_resolution_failure_at": { + "name": "last_actor_resolution_failure_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": { + "UQ_security_analysis_owner_state_org_owner": { + "name": "UQ_security_analysis_owner_state_org_owner", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_analysis_owner_state\".\"owned_by_organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_analysis_owner_state_user_owner": { + "name": "UQ_security_analysis_owner_state_user_owner", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_analysis_owner_state\".\"owned_by_user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_analysis_owner_state_owned_by_organization_id_organizations_id_fk": { + "name": "security_analysis_owner_state_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_analysis_owner_state", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_owner_state_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_analysis_owner_state_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_analysis_owner_state", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_analysis_owner_state_owner_check": { + "name": "security_analysis_owner_state_owner_check", + "value": "(\n (\"security_analysis_owner_state\".\"owned_by_user_id\" IS NOT NULL AND \"security_analysis_owner_state\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_analysis_owner_state\".\"owned_by_user_id\" IS NULL AND \"security_analysis_owner_state\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_analysis_owner_state_block_reason_check": { + "name": "security_analysis_owner_state_block_reason_check", + "value": "\"security_analysis_owner_state\".\"block_reason\" IS NULL OR \"security_analysis_owner_state\".\"block_reason\" IN ('INSUFFICIENT_CREDITS', 'ACTOR_RESOLUTION_FAILED', 'OPERATOR_PAUSE')" + } + }, + "isRLSEnabled": false + }, + "public.security_analysis_queue": { + "name": "security_analysis_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queue_status": { + "name": "queue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity_rank": { + "name": "severity_rank", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "admitted_config_revision": { + "name": "admitted_config_revision", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by_job_id": { + "name": "claimed_by_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reopen_requeue_count": { + "name": "reopen_requeue_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "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": { + "UQ_security_analysis_queue_finding_id": { + "name": "UQ_security_analysis_queue_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_claim_path_org": { + "name": "idx_security_analysis_queue_claim_path_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "severity_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_claim_path_user": { + "name": "idx_security_analysis_queue_claim_path_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "severity_rank", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_in_flight_org": { + "name": "idx_security_analysis_queue_in_flight_org", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queue_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_in_flight_user": { + "name": "idx_security_analysis_queue_in_flight_user", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queue_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_lag_dashboards": { + "name": "idx_security_analysis_queue_lag_dashboards", + "columns": [ + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_pending_reconciliation": { + "name": "idx_security_analysis_queue_pending_reconciliation", + "columns": [ + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_running_reconciliation": { + "name": "idx_security_analysis_queue_running_reconciliation", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"queue_status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_analysis_queue_failure_trend": { + "name": "idx_security_analysis_queue_failure_trend", + "columns": [ + { + "expression": "failure_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_analysis_queue\".\"failure_code\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_analysis_queue_finding_id_security_findings_id_fk": { + "name": "security_analysis_queue_finding_id_security_findings_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_queue_owned_by_organization_id_organizations_id_fk": { + "name": "security_analysis_queue_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_analysis_queue_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_analysis_queue_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_analysis_queue", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_analysis_queue_owner_check": { + "name": "security_analysis_queue_owner_check", + "value": "(\n (\"security_analysis_queue\".\"owned_by_user_id\" IS NOT NULL AND \"security_analysis_queue\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_analysis_queue\".\"owned_by_user_id\" IS NULL AND \"security_analysis_queue\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_analysis_queue_status_check": { + "name": "security_analysis_queue_status_check", + "value": "\"security_analysis_queue\".\"queue_status\" IN ('queued', 'pending', 'running', 'failed', 'completed')" + }, + "security_analysis_queue_claim_token_required_check": { + "name": "security_analysis_queue_claim_token_required_check", + "value": "\"security_analysis_queue\".\"queue_status\" NOT IN ('pending', 'running') OR \"security_analysis_queue\".\"claim_token\" IS NOT NULL" + }, + "security_analysis_queue_attempt_count_non_negative_check": { + "name": "security_analysis_queue_attempt_count_non_negative_check", + "value": "\"security_analysis_queue\".\"attempt_count\" >= 0" + }, + "security_analysis_queue_reopen_requeue_count_non_negative_check": { + "name": "security_analysis_queue_reopen_requeue_count_non_negative_check", + "value": "\"security_analysis_queue\".\"reopen_requeue_count\" >= 0" + }, + "security_analysis_queue_severity_rank_check": { + "name": "security_analysis_queue_severity_rank_check", + "value": "\"security_analysis_queue\".\"severity_rank\" IN (0, 1, 2, 3)" + }, + "security_analysis_queue_failure_code_check": { + "name": "security_analysis_queue_failure_code_check", + "value": "\"security_analysis_queue\".\"failure_code\" IS NULL OR \"security_analysis_queue\".\"failure_code\" IN (\n 'NETWORK_TIMEOUT',\n 'UPSTREAM_5XX',\n 'TEMP_TOKEN_FAILURE',\n 'START_CALL_AMBIGUOUS',\n 'REQUEUE_TEMPORARY_PRECONDITION',\n 'ACTOR_RESOLUTION_FAILED',\n 'GITHUB_TOKEN_UNAVAILABLE',\n 'INVALID_CONFIG',\n 'MISSING_OWNERSHIP',\n 'PERMISSION_DENIED_PERMANENT',\n 'UNSUPPORTED_SEVERITY',\n 'INSUFFICIENT_CREDITS',\n 'STATE_GUARD_REJECTED',\n 'SKIPPED_ALREADY_IN_PROGRESS',\n 'SKIPPED_NO_LONGER_ELIGIBLE',\n 'REOPEN_LOOP_GUARD',\n 'RUN_LOST'\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_audit_log": { + "name": "security_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "before_state": { + "name": "before_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "after_state": { + "name": "after_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "source_occurred_at": { + "name": "source_occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "finding_snapshot": { + "name": "finding_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_context": { + "name": "source_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_security_audit_log_org_created": { + "name": "IDX_security_audit_log_org_created", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_user_created": { + "name": "IDX_security_audit_log_user_created", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_resource": { + "name": "IDX_security_audit_log_resource", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_actor": { + "name": "IDX_security_audit_log_actor", + "columns": [ + { + "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": {} + }, + "IDX_security_audit_log_action": { + "name": "IDX_security_audit_log_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_audit_log_org_event_key": { + "name": "UQ_security_audit_log_org_event_key", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL AND \"security_audit_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_audit_log_user_event_key": { + "name": "UQ_security_audit_log_user_event_key", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_org_occurred": { + "name": "IDX_security_audit_log_org_occurred", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL AND \"security_audit_log\".\"occurred_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_security_audit_log_user_occurred": { + "name": "IDX_security_audit_log_user_occurred", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"occurred_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_audit_log_owned_by_organization_id_organizations_id_fk": { + "name": "security_audit_log_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_audit_log", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_audit_log_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_audit_log_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_audit_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_audit_log_owner_check": { + "name": "security_audit_log_owner_check", + "value": "(\"security_audit_log\".\"owned_by_user_id\" IS NOT NULL AND \"security_audit_log\".\"owned_by_organization_id\" IS NULL) OR (\"security_audit_log\".\"owned_by_user_id\" IS NULL AND \"security_audit_log\".\"owned_by_organization_id\" IS NOT NULL)" + }, + "security_audit_log_action_check": { + "name": "security_audit_log_action_check", + "value": "\"security_audit_log\".\"action\" IN ('security.finding.created', 'security.finding.severity_changed', 'security.finding.status_change', 'security.finding.dismissed', 'security.finding.auto_dismissed', 'security.finding.superseded', 'security.finding.analysis_started', 'security.finding.analysis_completed', 'security.finding.analysis_failed', 'security.remediation.queued', 'security.remediation.started', 'security.remediation.pr_opened', 'security.remediation.failed', 'security.remediation.blocked', 'security.remediation.no_changes_needed', 'security.remediation.cancelled', 'security.remediation.retried', 'security.finding.deleted', 'security.config.enabled', 'security.config.disabled', 'security.config.updated', 'security.sync.triggered', 'security.sync.completed', 'security.audit_log.exported', 'security.audit_report.generated')" + }, + "security_audit_log_actor_type_check": { + "name": "security_audit_log_actor_type_check", + "value": "\"security_audit_log\".\"actor_type\" IN ('customer_user', 'kilo_admin', 'system')" + }, + "security_audit_log_source_context_check": { + "name": "security_audit_log_source_context_check", + "value": "\"security_audit_log\".\"source_context\" IN ('security_sync', 'web', 'analysis_worker', 'remediation_callback', 'rollout_baseline')" + } + }, + "isRLSEnabled": false + }, + "public.security_finding_notifications": { + "name": "security_finding_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'staged'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "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": { + "uq_security_finding_notifications_finding_recipient_kind": { + "name": "uq_security_finding_notifications_finding_recipient_kind", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_pending": { + "name": "idx_security_finding_notifications_pending", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_finding_notifications\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_staged": { + "name": "idx_security_finding_notifications_staged", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_finding_notifications\".\"status\" = 'staged'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_finding_id": { + "name": "idx_security_finding_notifications_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_finding_notifications_recipient_user_id": { + "name": "idx_security_finding_notifications_recipient_user_id", + "columns": [ + { + "expression": "recipient_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_finding_notifications_finding_fk": { + "name": "security_finding_notifications_finding_fk", + "tableFrom": "security_finding_notifications", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_finding_notifications_recipient_fk": { + "name": "security_finding_notifications_recipient_fk", + "tableFrom": "security_finding_notifications", + "tableTo": "kilocode_users", + "columnsFrom": [ + "recipient_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_finding_notifications_kind_check": { + "name": "security_finding_notifications_kind_check", + "value": "\"security_finding_notifications\".\"kind\" IN ('new_finding', 'sla_warning', 'sla_breach')" + }, + "security_finding_notifications_status_check": { + "name": "security_finding_notifications_status_check", + "value": "\"security_finding_notifications\".\"status\" IN ('staged', 'pending', 'sending', 'sent', 'failed', 'cancelled')" + }, + "security_finding_notifications_attempt_count_check": { + "name": "security_finding_notifications_attempt_count_check", + "value": "\"security_finding_notifications\".\"attempt_count\" >= 0" + }, + "security_finding_notifications_claimed_at_check": { + "name": "security_finding_notifications_claimed_at_check", + "value": "(\n (\"security_finding_notifications\".\"status\" = 'sending' AND \"security_finding_notifications\".\"claimed_at\" IS NOT NULL) OR\n (\"security_finding_notifications\".\"status\" <> 'sending' AND \"security_finding_notifications\".\"claimed_at\" IS NULL)\n )" + }, + "security_finding_notifications_sent_at_check": { + "name": "security_finding_notifications_sent_at_check", + "value": "(\n (\"security_finding_notifications\".\"status\" = 'sent' AND \"security_finding_notifications\".\"sent_at\" IS NOT NULL) OR\n (\"security_finding_notifications\".\"status\" <> 'sent' AND \"security_finding_notifications\".\"sent_at\" IS NULL)\n )" + }, + "security_finding_notifications_error_message_length_check": { + "name": "security_finding_notifications_error_message_length_check", + "value": "\"security_finding_notifications\".\"error_message\" IS NULL OR length(\"security_finding_notifications\".\"error_message\") <= 500" + } + }, + "isRLSEnabled": false + }, + "public.security_findings": { + "name": "security_findings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ghsa_id": { + "name": "ghsa_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cve_id": { + "name": "cve_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_ecosystem": { + "name": "package_ecosystem", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vulnerable_version_range": { + "name": "vulnerable_version_range", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "patched_version": { + "name": "patched_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manifest_path": { + "name": "manifest_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "ignored_reason": { + "name": "ignored_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ignored_by": { + "name": "ignored_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fixed_at": { + "name": "fixed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sla_due_at": { + "name": "sla_due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dependabot_html_url": { + "name": "dependabot_html_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwe_ids": { + "name": "cwe_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "cvss_score": { + "name": "cvss_score", + "type": "numeric(3, 1)", + "primaryKey": false, + "notNull": false + }, + "dependency_scope": { + "name": "dependency_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_session_id": { + "name": "cli_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_status": { + "name": "analysis_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_started_at": { + "name": "analysis_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "analysis_completed_at": { + "name": "analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "analysis_error": { + "name": "analysis_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis": { + "name": "analysis", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_detected_at": { + "name": "first_detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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": { + "uq_security_findings_user_source": { + "name": "uq_security_findings_user_source", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_findings\".\"owned_by_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_security_findings_org_source": { + "name": "uq_security_findings_org_source", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_findings\".\"owned_by_organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_org_id": { + "name": "idx_security_findings_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_user_id": { + "name": "idx_security_findings_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_repo": { + "name": "idx_security_findings_repo", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_severity": { + "name": "idx_security_findings_severity", + "columns": [ + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_status": { + "name": "idx_security_findings_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_package": { + "name": "idx_security_findings_package", + "columns": [ + { + "expression": "package_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_sla_due_at": { + "name": "idx_security_findings_sla_due_at", + "columns": [ + { + "expression": "sla_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_session_id": { + "name": "idx_security_findings_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_cli_session_id": { + "name": "idx_security_findings_cli_session_id", + "columns": [ + { + "expression": "cli_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_analysis_status": { + "name": "idx_security_findings_analysis_status", + "columns": [ + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_org_analysis_in_flight": { + "name": "idx_security_findings_org_analysis_in_flight", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_findings\".\"analysis_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_findings_user_analysis_in_flight": { + "name": "idx_security_findings_user_analysis_in_flight", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_findings\".\"analysis_status\" IN ('pending', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_findings_owned_by_organization_id_organizations_id_fk": { + "name": "security_findings_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_findings", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_findings_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_findings_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_findings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_findings_platform_integration_id_platform_integrations_id_fk": { + "name": "security_findings_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "security_findings", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_findings_owner_check": { + "name": "security_findings_owner_check", + "value": "(\n (\"security_findings\".\"owned_by_user_id\" IS NOT NULL AND \"security_findings\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_findings\".\"owned_by_user_id\" IS NULL AND \"security_findings\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.security_remediation_attempts": { + "name": "security_remediation_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "remediation_id": { + "name": "remediation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "retry_of_attempt_id": { + "name": "retry_of_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "analysis_fingerprint": { + "name": "analysis_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "analysis_completed_at": { + "name": "analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "remediation_model_slug": { + "name": "remediation_model_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kilo_session_id": { + "name": "kilo_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "claim_token": { + "name": "claim_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by_job_id": { + "name": "claimed_by_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "launch_attempt_count": { + "name": "launch_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "callback_attempt_token_hash": { + "name": "callback_attempt_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "structured_result": { + "name": "structured_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "final_assistant_message": { + "name": "final_assistant_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "validation_evidence": { + "name": "validation_evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "risk_notes": { + "name": "risk_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_reason": { + "name": "draft_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_draft": { + "name": "pr_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pr_head_branch": { + "name": "pr_head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_base_branch": { + "name": "pr_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_at": { + "name": "cancellation_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancellation_requested_by_user_id": { + "name": "cancellation_requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "launched_at": { + "name": "launched_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": { + "UQ_security_remediation_attempts_number": { + "name": "UQ_security_remediation_attempts_number", + "columns": [ + { + "expression": "remediation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_active_finding": { + "name": "UQ_security_remediation_attempts_active_finding", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_active_remediation": { + "name": "UQ_security_remediation_attempts_active_remediation", + "columns": [ + { + "expression": "remediation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_security_remediation_attempts_finding_fingerprint_terminal": { + "name": "UQ_security_remediation_attempts_finding_fingerprint_terminal", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running', 'pr_opened')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_org_claim": { + "name": "idx_security_remediation_attempts_org_claim", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_user_claim": { + "name": "idx_security_remediation_attempts_user_claim", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_repo_claim": { + "name": "idx_security_remediation_attempts_repo_claim", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_org_inflight": { + "name": "idx_security_remediation_attempts_org_inflight", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_user_inflight": { + "name": "idx_security_remediation_attempts_user_inflight", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_repo_inflight": { + "name": "idx_security_remediation_attempts_repo_inflight", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claimed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"security_remediation_attempts\".\"status\" IN ('launching', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_cloud_agent_session": { + "name": "idx_security_remediation_attempts_cloud_agent_session", + "columns": [ + { + "expression": "cloud_agent_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediation_attempts_finding_fingerprint": { + "name": "idx_security_remediation_attempts_finding_fingerprint", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "analysis_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_remediation_attempts_remediation_id_security_remediations_id_fk": { + "name": "security_remediation_attempts_remediation_id_security_remediations_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "security_remediations", + "columnsFrom": [ + "remediation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_finding_id_security_findings_id_fk": { + "name": "security_remediation_attempts_finding_id_security_findings_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_owned_by_organization_id_organizations_id_fk": { + "name": "security_remediation_attempts_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediation_attempts_requested_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_requested_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "security_remediation_attempts_cancellation_requested_by_user_id_kilocode_users_id_fk": { + "name": "security_remediation_attempts_cancellation_requested_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediation_attempts", + "tableTo": "kilocode_users", + "columnsFrom": [ + "cancellation_requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_remediation_attempts_owner_check": { + "name": "security_remediation_attempts_owner_check", + "value": "(\n (\"security_remediation_attempts\".\"owned_by_user_id\" IS NOT NULL AND \"security_remediation_attempts\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_remediation_attempts\".\"owned_by_user_id\" IS NULL AND \"security_remediation_attempts\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_remediation_attempts_status_check": { + "name": "security_remediation_attempts_status_check", + "value": "\"security_remediation_attempts\".\"status\" IN ('queued', 'launching', 'running', 'pr_opened', 'failed', 'blocked', 'no_changes_needed', 'cancelled')" + }, + "security_remediation_attempts_origin_check": { + "name": "security_remediation_attempts_origin_check", + "value": "\"security_remediation_attempts\".\"origin\" IN ('auto_policy', 'bulk_existing', 'manual')" + }, + "security_remediation_attempts_attempt_number_check": { + "name": "security_remediation_attempts_attempt_number_check", + "value": "\"security_remediation_attempts\".\"attempt_number\" >= 1" + }, + "security_remediation_attempts_launch_attempt_count_check": { + "name": "security_remediation_attempts_launch_attempt_count_check", + "value": "\"security_remediation_attempts\".\"launch_attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.security_remediations": { + "name": "security_remediations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finding_id": { + "name": "finding_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "latest_attempt_id": { + "name": "latest_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_analysis_fingerprint": { + "name": "latest_analysis_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_analysis_completed_at": { + "name": "latest_analysis_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pr_draft": { + "name": "pr_draft", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "pr_head_branch": { + "name": "pr_head_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_base_branch": { + "name": "pr_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_summary": { + "name": "outcome_summary", + "type": "text", + "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": { + "UQ_security_remediations_finding_id": { + "name": "UQ_security_remediations_finding_id", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_org_status": { + "name": "idx_security_remediations_org_status", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_user_status": { + "name": "idx_security_remediations_user_status", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_repo_status": { + "name": "idx_security_remediations_repo_status", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_security_remediations_latest_attempt": { + "name": "idx_security_remediations_latest_attempt", + "columns": [ + { + "expression": "latest_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "security_remediations_owned_by_organization_id_organizations_id_fk": { + "name": "security_remediations_owned_by_organization_id_organizations_id_fk", + "tableFrom": "security_remediations", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediations_owned_by_user_id_kilocode_users_id_fk": { + "name": "security_remediations_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "security_remediations", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "security_remediations_finding_id_security_findings_id_fk": { + "name": "security_remediations_finding_id_security_findings_id_fk", + "tableFrom": "security_remediations", + "tableTo": "security_findings", + "columnsFrom": [ + "finding_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "security_remediations_owner_check": { + "name": "security_remediations_owner_check", + "value": "(\n (\"security_remediations\".\"owned_by_user_id\" IS NOT NULL AND \"security_remediations\".\"owned_by_organization_id\" IS NULL) OR\n (\"security_remediations\".\"owned_by_user_id\" IS NULL AND \"security_remediations\".\"owned_by_organization_id\" IS NOT NULL)\n )" + }, + "security_remediations_status_check": { + "name": "security_remediations_status_check", + "value": "\"security_remediations\".\"status\" IN ('queued', 'running', 'pr_opened', 'failed', 'blocked', 'no_changes_needed', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.shared_cli_sessions": { + "name": "shared_cli_sessions", + "schema": "", + "columns": { + "share_id": { + "name": "share_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared_state": { + "name": "shared_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "api_conversation_history_blob_url": { + "name": "api_conversation_history_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_metadata_blob_url": { + "name": "task_metadata_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ui_messages_blob_url": { + "name": "ui_messages_blob_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_state_blob_url": { + "name": "git_state_blob_url", + "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": { + "IDX_shared_cli_sessions_session_id": { + "name": "IDX_shared_cli_sessions_session_id", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_shared_cli_sessions_created_at": { + "name": "IDX_shared_cli_sessions_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shared_cli_sessions_session_id_cli_sessions_session_id_fk": { + "name": "shared_cli_sessions_session_id_cli_sessions_session_id_fk", + "tableFrom": "shared_cli_sessions", + "tableTo": "cli_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "session_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "shared_cli_sessions_kilo_user_id_kilocode_users_id_fk": { + "name": "shared_cli_sessions_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "shared_cli_sessions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "shared_cli_sessions_shared_state_check": { + "name": "shared_cli_sessions_shared_state_check", + "value": "\"shared_cli_sessions\".\"shared_state\" IN ('public', 'organization')" + } + }, + "isRLSEnabled": false + }, + "public.slack_bot_requests": { + "name": "slack_bot_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "slack_team_id": { + "name": "slack_team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_team_name": { + "name": "slack_team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_channel_id": { + "name": "slack_channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_thread_ts": { + "name": "slack_thread_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message": { + "name": "user_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_message_truncated": { + "name": "user_message_truncated", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_calls_made": { + "name": "tool_calls_made", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "cloud_agent_session_id": { + "name": "cloud_agent_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_slack_bot_requests_created_at": { + "name": "idx_slack_bot_requests_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_slack_team_id": { + "name": "idx_slack_bot_requests_slack_team_id", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_owned_by_org_id": { + "name": "idx_slack_bot_requests_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_owned_by_user_id": { + "name": "idx_slack_bot_requests_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_status": { + "name": "idx_slack_bot_requests_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_event_type": { + "name": "idx_slack_bot_requests_event_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_slack_bot_requests_team_created": { + "name": "idx_slack_bot_requests_team_created", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_bot_requests_owned_by_organization_id_organizations_id_fk": { + "name": "slack_bot_requests_owned_by_organization_id_organizations_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_bot_requests_owned_by_user_id_kilocode_users_id_fk": { + "name": "slack_bot_requests_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_bot_requests_platform_integration_id_platform_integrations_id_fk": { + "name": "slack_bot_requests_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "slack_bot_requests", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_bot_requests_owner_check": { + "name": "slack_bot_requests_owner_check", + "value": "(\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NOT NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NULL) OR\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NOT NULL) OR\n (\"slack_bot_requests\".\"owned_by_user_id\" IS NULL AND \"slack_bot_requests\".\"owned_by_organization_id\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.slack_oauth_credentials": { + "name": "slack_oauth_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "platform_integration_id": { + "name": "platform_integration_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slack_team_id": { + "name": "slack_team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_enterprise_id": { + "name": "slack_enterprise_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_enterprise_install": { + "name": "is_enterprise_install", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "refresh_claimed_at": { + "name": "refresh_claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_attempt_count": { + "name": "refresh_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_refresh_attempt_at": { + "name": "next_refresh_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_slack_oauth_credentials_platform_integration_id": { + "name": "UQ_slack_oauth_credentials_platform_integration_id", + "columns": [ + { + "expression": "platform_integration_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_slack_oauth_credentials_slack_team_id": { + "name": "IDX_slack_oauth_credentials_slack_team_id", + "columns": [ + { + "expression": "slack_team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_slack_oauth_credentials_refresh_due": { + "name": "IDX_slack_oauth_credentials_refresh_due", + "columns": [ + { + "expression": "access_token_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"slack_oauth_credentials\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_oauth_credentials_platform_integration_id_platform_integrations_id_fk": { + "name": "slack_oauth_credentials_platform_integration_id_platform_integrations_id_fk", + "tableFrom": "slack_oauth_credentials", + "tableTo": "platform_integrations", + "columnsFrom": [ + "platform_integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_oauth_credentials_credential_version_check": { + "name": "slack_oauth_credentials_credential_version_check", + "value": "\"slack_oauth_credentials\".\"credential_version\" > 0" + }, + "slack_oauth_credentials_refresh_attempt_count_check": { + "name": "slack_oauth_credentials_refresh_attempt_count_check", + "value": "\"slack_oauth_credentials\".\"refresh_attempt_count\" >= 0" + }, + "slack_oauth_credentials_slack_team_id_check": { + "name": "slack_oauth_credentials_slack_team_id_check", + "value": "\"slack_oauth_credentials\".\"slack_team_id\" <> ''" + } + }, + "isRLSEnabled": false + }, + "public.source_embeddings": { + "name": "source_embeddings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_line": { + "name": "start_line", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_line": { + "name": "end_line", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "git_branch": { + "name": "git_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "is_base_branch": { + "name": "is_base_branch", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_source_embeddings_organization_id": { + "name": "IDX_source_embeddings_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_kilo_user_id": { + "name": "IDX_source_embeddings_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_project_id": { + "name": "IDX_source_embeddings_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_created_at": { + "name": "IDX_source_embeddings_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_updated_at": { + "name": "IDX_source_embeddings_updated_at", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_file_path_lower": { + "name": "IDX_source_embeddings_file_path_lower", + "columns": [ + { + "expression": "LOWER(\"file_path\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_git_branch": { + "name": "IDX_source_embeddings_git_branch", + "columns": [ + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_source_embeddings_org_project_branch": { + "name": "IDX_source_embeddings_org_project_branch", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "git_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "source_embeddings_organization_id_organizations_id_fk": { + "name": "source_embeddings_organization_id_organizations_id_fk", + "tableFrom": "source_embeddings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "source_embeddings_kilo_user_id_kilocode_users_id_fk": { + "name": "source_embeddings_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "source_embeddings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_source_embeddings_org_project_branch_file_lines": { + "name": "UQ_source_embeddings_org_project_branch_file_lines", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "project_id", + "git_branch", + "file_path", + "start_line", + "end_line" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spend_alert_deliveries": { + "name": "spend_alert_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fired_at": { + "name": "fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "recipients": { + "name": "recipients", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_error_redacted": { + "name": "last_error_redacted", + "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": { + "IDX_spend_alert_deliveries_pending": { + "name": "IDX_spend_alert_deliveries_pending", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_count", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spend_alert_deliveries_rule_id_spend_alert_rules_id_fk": { + "name": "spend_alert_deliveries_rule_id_spend_alert_rules_id_fk", + "tableFrom": "spend_alert_deliveries", + "tableTo": "spend_alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_spend_alert_deliveries_dedupe_key": { + "name": "UQ_spend_alert_deliveries_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spend_alert_hourly": { + "name": "spend_alert_hourly", + "schema": "", + "columns": { + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hour_start": { + "name": "hour_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cost_microdollars": { + "name": "cost_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_spend_alert_hourly_scope_hour": { + "name": "uq_spend_alert_hourly_scope_hour", + "columns": [ + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_spend_alert_hourly_hour_start": { + "name": "IDX_spend_alert_hourly_hour_start", + "columns": [ + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spend_alert_rule_state": { + "name": "spend_alert_rule_state", + "schema": "", + "columns": { + "rule_id": { + "name": "rule_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "firing": { + "name": "firing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "condition_started_at": { + "name": "condition_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_value_microdollars": { + "name": "last_value_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "spend_alert_rule_state_rule_id_spend_alert_rules_id_fk": { + "name": "spend_alert_rule_state_rule_id_spend_alert_rules_id_fk", + "tableFrom": "spend_alert_rule_state", + "tableTo": "spend_alert_rules", + "columnsFrom": [ + "rule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spend_alert_rules": { + "name": "spend_alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "settings_id": { + "name": "settings_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "threshold_microdollars": { + "name": "threshold_microdollars", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "window_hours": { + "name": "window_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "multiplier_basis_points": { + "name": "multiplier_basis_points", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "email_enabled": { + "name": "email_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "push_enabled": { + "name": "push_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": { + "uq_spend_alert_rules_kind": { + "name": "uq_spend_alert_rules_kind", + "columns": [ + { + "expression": "settings_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spend_alert_rules_settings_id_spend_alert_settings_id_fk": { + "name": "spend_alert_rules_settings_id_spend_alert_settings_id_fk", + "tableFrom": "spend_alert_rules", + "tableTo": "spend_alert_settings", + "columnsFrom": [ + "settings_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spend_alert_settings": { + "name": "spend_alert_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "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": { + "uq_spend_alert_settings_scope": { + "name": "uq_spend_alert_settings_scope", + "columns": [ + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "spend_alert_settings_kilo_user_id_kilocode_users_id_fk": { + "name": "spend_alert_settings_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "spend_alert_settings", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spend_alert_settings_organization_id_organizations_id_fk": { + "name": "spend_alert_settings_organization_id_organizations_id_fk", + "tableFrom": "spend_alert_settings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "spend_alert_settings_scope_check": { + "name": "spend_alert_settings_scope_check", + "value": "(\"spend_alert_settings\".\"kilo_user_id\" IS NOT NULL) <> (\"spend_alert_settings\".\"organization_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.stripe_dispute_actions": { + "name": "stripe_dispute_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_reference_id": { + "name": "result_reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "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": { + "IDX_stripe_dispute_actions_case_id": { + "name": "IDX_stripe_dispute_actions_case_id", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_actions_claim_path": { + "name": "IDX_stripe_dispute_actions_claim_path", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_dispute_actions_case_id_stripe_dispute_cases_id_fk": { + "name": "stripe_dispute_actions_case_id_stripe_dispute_cases_id_fk", + "tableFrom": "stripe_dispute_actions", + "tableTo": "stripe_dispute_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_dispute_actions_case_type_target": { + "name": "UQ_stripe_dispute_actions_case_type_target", + "nullsNotDistinct": false, + "columns": [ + "case_id", + "action_type", + "target_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_dispute_actions_action_type_check": { + "name": "stripe_dispute_actions_action_type_check", + "value": "\"stripe_dispute_actions\".\"action_type\" IN ('stripe_acceptance', 'user_block', 'auto_top_up_disable', 'credit_balance_reset', 'subscription_cancellation', 'access_termination', 'kiloclaw_suspension')" + }, + "stripe_dispute_actions_status_check": { + "name": "stripe_dispute_actions_status_check", + "value": "\"stripe_dispute_actions\".\"status\" IN ('queued', 'processing', 'completed', 'failed', 'skipped')" + }, + "stripe_dispute_actions_attempt_count_non_negative_check": { + "name": "stripe_dispute_actions_attempt_count_non_negative_check", + "value": "\"stripe_dispute_actions\".\"attempt_count\" >= 0" + }, + "stripe_dispute_actions_target_key_not_empty_check": { + "name": "stripe_dispute_actions_target_key_not_empty_check", + "value": "length(\"stripe_dispute_actions\".\"target_key\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_dispute_cases": { + "name": "stripe_dispute_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_dispute_id": { + "name": "stripe_dispute_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_event_created_at": { + "name": "stripe_event_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dispute_reason": { + "name": "dispute_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_status": { + "name": "stripe_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_classification": { + "name": "owner_classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'needs_action'" + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_created_at": { + "name": "stripe_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evidence_due_by": { + "name": "evidence_due_by", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_by_kilo_user_id": { + "name": "accepted_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acceptance_started_at": { + "name": "acceptance_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enforcement_completed_at": { + "name": "enforcement_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_required_at": { + "name": "review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_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": { + "IDX_stripe_dispute_cases_event_id": { + "name": "IDX_stripe_dispute_cases_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_charge_id": { + "name": "IDX_stripe_dispute_cases_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_payment_intent_id": { + "name": "IDX_stripe_dispute_cases_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_customer_id": { + "name": "IDX_stripe_dispute_cases_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_kilo_user_id": { + "name": "IDX_stripe_dispute_cases_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_organization_id": { + "name": "IDX_stripe_dispute_cases_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_dispute_cases_status_due_by": { + "name": "IDX_stripe_dispute_cases_status_due_by", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "evidence_due_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stripe_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_dispute_cases_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_dispute_cases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_dispute_cases_organization_id_organizations_id_fk": { + "name": "stripe_dispute_cases_organization_id_organizations_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_dispute_cases_accepted_by_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_dispute_cases_accepted_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_dispute_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "accepted_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_dispute_cases_dispute_id": { + "name": "UQ_stripe_dispute_cases_dispute_id", + "nullsNotDistinct": false, + "columns": [ + "stripe_dispute_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_dispute_cases_owner_classification_check": { + "name": "stripe_dispute_cases_owner_classification_check", + "value": "\"stripe_dispute_cases\".\"owner_classification\" IN ('personal', 'organization', 'ambiguous', 'unmatched')" + }, + "stripe_dispute_cases_status_check": { + "name": "stripe_dispute_cases_status_check", + "value": "\"stripe_dispute_cases\".\"status\" IN ('needs_action', 'processing', 'accepted', 'acceptance_failed', 'enforcement_failed', 'review_required', 'closed')" + }, + "stripe_dispute_cases_amount_minor_units_non_negative_check": { + "name": "stripe_dispute_cases_amount_minor_units_non_negative_check", + "value": "\"stripe_dispute_cases\".\"amount_minor_units\" IS NULL OR \"stripe_dispute_cases\".\"amount_minor_units\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_early_fraud_warning_actions": { + "name": "stripe_early_fraud_warning_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "terminal_at": { + "name": "terminal_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result_code": { + "name": "result_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_reference_id": { + "name": "result_reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "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": { + "IDX_stripe_early_fraud_warning_actions_case_id": { + "name": "IDX_stripe_early_fraud_warning_actions_case_id", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_actions_claim_path": { + "name": "IDX_stripe_early_fraud_warning_actions_claim_path", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "stripe_early_fraud_warning_actions_case_id_stripe_early_fraud_warning_cases_id_fk": { + "name": "stripe_early_fraud_warning_actions_case_id_stripe_early_fraud_warning_cases_id_fk", + "tableFrom": "stripe_early_fraud_warning_actions", + "tableTo": "stripe_early_fraud_warning_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_early_fraud_warning_actions_case_type_target": { + "name": "UQ_stripe_early_fraud_warning_actions_case_type_target", + "nullsNotDistinct": false, + "columns": [ + "case_id", + "action_type", + "target_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_early_fraud_warning_actions_action_type_check": { + "name": "stripe_early_fraud_warning_actions_action_type_check", + "value": "\"stripe_early_fraud_warning_actions\".\"action_type\" IN ('containment', 'refund', 'payment_value_clawback', 'subscription_termination', 'access_termination', 'kiloclaw_suspension', 'affiliate_payout_reversal', 'referral_reward_reversal', 'user_notice')" + }, + "stripe_early_fraud_warning_actions_status_check": { + "name": "stripe_early_fraud_warning_actions_status_check", + "value": "\"stripe_early_fraud_warning_actions\".\"status\" IN ('queued', 'processing', 'completed', 'failed', 'review_required', 'dismissed')" + }, + "stripe_early_fraud_warning_actions_attempt_count_non_negative_check": { + "name": "stripe_early_fraud_warning_actions_attempt_count_non_negative_check", + "value": "\"stripe_early_fraud_warning_actions\".\"attempt_count\" >= 0" + }, + "stripe_early_fraud_warning_actions_target_key_not_empty_check": { + "name": "stripe_early_fraud_warning_actions_target_key_not_empty_check", + "value": "length(\"stripe_early_fraud_warning_actions\".\"target_key\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_early_fraud_warning_cases": { + "name": "stripe_early_fraud_warning_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "stripe_early_fraud_warning_id": { + "name": "stripe_early_fraud_warning_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_event_id": { + "name": "stripe_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_minor_units": { + "name": "amount_minor_units", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_classification": { + "name": "owner_classification", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_context": { + "name": "failure_context", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "warning_created_at": { + "name": "warning_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "contained_at": { + "name": "contained_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_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 + }, + "review_required_at": { + "name": "review_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remediated_at": { + "name": "remediated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dismissed_at": { + "name": "dismissed_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": { + "IDX_stripe_early_fraud_warning_cases_event_id": { + "name": "IDX_stripe_early_fraud_warning_cases_event_id", + "columns": [ + { + "expression": "stripe_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_charge_id": { + "name": "IDX_stripe_early_fraud_warning_cases_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_payment_intent_id": { + "name": "IDX_stripe_early_fraud_warning_cases_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_customer_id": { + "name": "IDX_stripe_early_fraud_warning_cases_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_kilo_user_id": { + "name": "IDX_stripe_early_fraud_warning_cases_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_organization_id": { + "name": "IDX_stripe_early_fraud_warning_cases_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_stripe_early_fraud_warning_cases_status_created_at": { + "name": "IDX_stripe_early_fraud_warning_cases_status_created_at", + "columns": [ + { + "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": { + "stripe_early_fraud_warning_cases_kilo_user_id_kilocode_users_id_fk": { + "name": "stripe_early_fraud_warning_cases_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "stripe_early_fraud_warning_cases", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "stripe_early_fraud_warning_cases_organization_id_organizations_id_fk": { + "name": "stripe_early_fraud_warning_cases_organization_id_organizations_id_fk", + "tableFrom": "stripe_early_fraud_warning_cases", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_stripe_early_fraud_warning_cases_warning_id": { + "name": "UQ_stripe_early_fraud_warning_cases_warning_id", + "nullsNotDistinct": false, + "columns": [ + "stripe_early_fraud_warning_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "stripe_early_fraud_warning_cases_owner_classification_check": { + "name": "stripe_early_fraud_warning_cases_owner_classification_check", + "value": "\"stripe_early_fraud_warning_cases\".\"owner_classification\" IN ('personal', 'organization', 'ambiguous', 'unmatched')" + }, + "stripe_early_fraud_warning_cases_status_check": { + "name": "stripe_early_fraud_warning_cases_status_check", + "value": "\"stripe_early_fraud_warning_cases\".\"status\" IN ('queued', 'contained', 'processing', 'completed', 'review_required', 'failed', 'remediated', 'dismissed')" + }, + "stripe_early_fraud_warning_cases_amount_minor_units_non_negative_check": { + "name": "stripe_early_fraud_warning_cases_amount_minor_units_non_negative_check", + "value": "\"stripe_early_fraud_warning_cases\".\"amount_minor_units\" IS NULL OR \"stripe_early_fraud_warning_cases\".\"amount_minor_units\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.stripe_service_fee_assessments": { + "name": "stripe_service_fee_assessments", + "schema": "", + "columns": { + "assessment_key": { + "name": "assessment_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flow": { + "name": "flow", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_checkout_session_id": { + "name": "stripe_checkout_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_fee_price_id": { + "name": "stripe_fee_price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_checkout_fee_line_item_id": { + "name": "stripe_checkout_fee_line_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_fee_item_id": { + "name": "stripe_invoice_fee_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_fee_line_item_id": { + "name": "stripe_invoice_fee_line_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "eligibility_created_at": { + "name": "eligibility_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "eligible_subtotal_minor": { + "name": "eligible_subtotal_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expected_fee_minor": { + "name": "expected_fee_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "charged_fee_minor": { + "name": "charged_fee_minor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "gross_paid_minor": { + "name": "gross_paid_minor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "settled_product_minor": { + "name": "settled_product_minor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "refunded_product_minor": { + "name": "refunded_product_minor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "refunded_fee_minor": { + "name": "refunded_fee_minor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "refunded_gross_minor": { + "name": "refunded_gross_minor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "disputed_fee_minor": { + "name": "disputed_fee_minor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "exemption_id": { + "name": "exemption_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_stripe_service_fee_assessments_stripe_customer_id": { + "name": "IDX_stripe_service_fee_assessments_stripe_customer_id", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_stripe_service_fee_assessments_checkout_session_id": { + "name": "UQ_stripe_service_fee_assessments_checkout_session_id", + "columns": [ + { + "expression": "stripe_checkout_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"stripe_service_fee_assessments\".\"stripe_checkout_session_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_stripe_service_fee_assessments_stripe_invoice_id": { + "name": "UQ_stripe_service_fee_assessments_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"stripe_service_fee_assessments\".\"stripe_invoice_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_stripe_service_fee_assessments_stripe_payment_intent_id": { + "name": "UQ_stripe_service_fee_assessments_stripe_payment_intent_id", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"stripe_service_fee_assessments\".\"stripe_payment_intent_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_stripe_service_fee_assessments_stripe_charge_id": { + "name": "UQ_stripe_service_fee_assessments_stripe_charge_id", + "columns": [ + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"stripe_service_fee_assessments\".\"stripe_charge_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_stripe_service_fee_assessments_checkout_fee_line_item_id": { + "name": "UQ_stripe_service_fee_assessments_checkout_fee_line_item_id", + "columns": [ + { + "expression": "stripe_checkout_fee_line_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"stripe_service_fee_assessments\".\"stripe_checkout_fee_line_item_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_stripe_service_fee_assessments_invoice_fee_item_id": { + "name": "UQ_stripe_service_fee_assessments_invoice_fee_item_id", + "columns": [ + { + "expression": "stripe_invoice_fee_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"stripe_service_fee_assessments\".\"stripe_invoice_fee_item_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_stripe_service_fee_assessments_invoice_fee_line_item_id": { + "name": "UQ_stripe_service_fee_assessments_invoice_fee_line_item_id", + "columns": [ + { + "expression": "stripe_invoice_fee_line_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"stripe_service_fee_assessments\".\"stripe_invoice_fee_line_item_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "FK_stripe_svc_fee_assessments_kilo_user": { + "name": "FK_stripe_svc_fee_assessments_kilo_user", + "tableFrom": "stripe_service_fee_assessments", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "FK_stripe_svc_fee_assessments_organization": { + "name": "FK_stripe_svc_fee_assessments_organization", + "tableFrom": "stripe_service_fee_assessments", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "FK_stripe_svc_fee_assessments_exemption": { + "name": "FK_stripe_svc_fee_assessments_exemption", + "tableFrom": "stripe_service_fee_assessments", + "tableTo": "organization_service_fee_exemptions", + "columnsFrom": [ + "exemption_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "stripe_service_fee_assessments_flow_check": { + "name": "stripe_service_fee_assessments_flow_check", + "value": "\"stripe_service_fee_assessments\".\"flow\" IN ('personal_top_up', 'organization_top_up', 'personal_auto_top_up_setup', 'organization_auto_top_up_setup', 'personal_auto_top_up', 'organization_auto_top_up', 'personal_kilo_pass', 'organization_kilo_pass')" + }, + "stripe_service_fee_assessments_outcome_check": { + "name": "stripe_service_fee_assessments_outcome_check", + "value": "\"stripe_service_fee_assessments\".\"outcome\" IN ('pending', 'charged', 'exempt', 'pre_activation', 'zero_rounded', 'unsupported_currency', 'missed')" + }, + "stripe_service_fee_assessments_currency_check": { + "name": "stripe_service_fee_assessments_currency_check", + "value": "\"stripe_service_fee_assessments\".\"currency\" ~ '^[a-z]{3}$'" + }, + "stripe_service_fee_assessments_owner_check": { + "name": "stripe_service_fee_assessments_owner_check", + "value": "(\n \"stripe_service_fee_assessments\".\"flow\" LIKE 'personal_%'\n AND \"stripe_service_fee_assessments\".\"kilo_user_id\" IS NOT NULL\n AND \"stripe_service_fee_assessments\".\"organization_id\" IS NULL\n ) OR (\n \"stripe_service_fee_assessments\".\"flow\" LIKE 'organization_%'\n AND \"stripe_service_fee_assessments\".\"organization_id\" IS NOT NULL\n )" + }, + "stripe_service_fee_assessments_amounts_nonnegative_check": { + "name": "stripe_service_fee_assessments_amounts_nonnegative_check", + "value": "\"stripe_service_fee_assessments\".\"eligible_subtotal_minor\" >= 0\n AND \"stripe_service_fee_assessments\".\"expected_fee_minor\" >= 0\n AND \"stripe_service_fee_assessments\".\"charged_fee_minor\" >= 0\n AND \"stripe_service_fee_assessments\".\"gross_paid_minor\" >= 0\n AND \"stripe_service_fee_assessments\".\"settled_product_minor\" >= 0\n AND \"stripe_service_fee_assessments\".\"refunded_product_minor\" >= 0\n AND \"stripe_service_fee_assessments\".\"refunded_fee_minor\" >= 0\n AND \"stripe_service_fee_assessments\".\"refunded_gross_minor\" >= 0\n AND \"stripe_service_fee_assessments\".\"disputed_fee_minor\" >= 0" + }, + "stripe_service_fee_assessments_refund_fee_check": { + "name": "stripe_service_fee_assessments_refund_fee_check", + "value": "\"stripe_service_fee_assessments\".\"refunded_fee_minor\" <= \"stripe_service_fee_assessments\".\"charged_fee_minor\"" + }, + "stripe_service_fee_assessments_refund_product_check": { + "name": "stripe_service_fee_assessments_refund_product_check", + "value": "\"stripe_service_fee_assessments\".\"refunded_product_minor\" <= \"stripe_service_fee_assessments\".\"settled_product_minor\"" + }, + "stripe_service_fee_assessments_disputed_fee_check": { + "name": "stripe_service_fee_assessments_disputed_fee_check", + "value": "\"stripe_service_fee_assessments\".\"disputed_fee_minor\" <= \"stripe_service_fee_assessments\".\"charged_fee_minor\"" + }, + "stripe_service_fee_assessments_pending_check": { + "name": "stripe_service_fee_assessments_pending_check", + "value": "\"stripe_service_fee_assessments\".\"outcome\" <> 'pending'\n OR (\"stripe_service_fee_assessments\".\"charged_fee_minor\" = 0 AND \"stripe_service_fee_assessments\".\"settled_at\" IS NULL)" + }, + "stripe_service_fee_assessments_charged_check": { + "name": "stripe_service_fee_assessments_charged_check", + "value": "\"stripe_service_fee_assessments\".\"outcome\" <> 'charged' OR (\n (\n \"stripe_service_fee_assessments\".\"stripe_invoice_fee_item_id\" IS NOT NULL\n OR \"stripe_service_fee_assessments\".\"stripe_invoice_fee_line_item_id\" IS NOT NULL\n OR \"stripe_service_fee_assessments\".\"stripe_checkout_fee_line_item_id\" IS NOT NULL\n OR \"stripe_service_fee_assessments\".\"settled_at\" IS NOT NULL\n )\n AND (\"stripe_service_fee_assessments\".\"charged_fee_minor\" <> 0 OR \"stripe_service_fee_assessments\".\"settled_product_minor\" = 0)\n )" + }, + "stripe_service_fee_assessments_missed_check": { + "name": "stripe_service_fee_assessments_missed_check", + "value": "\"stripe_service_fee_assessments\".\"outcome\" <> 'missed' OR (\n \"stripe_service_fee_assessments\".\"expected_fee_minor\" > 0\n AND \"stripe_service_fee_assessments\".\"charged_fee_minor\" = 0\n AND \"stripe_service_fee_assessments\".\"failure_code\" IS NOT NULL\n AND length(trim(\"stripe_service_fee_assessments\".\"failure_code\")) > 0\n )" + }, + "stripe_service_fee_assessments_zero_rounded_check": { + "name": "stripe_service_fee_assessments_zero_rounded_check", + "value": "\"stripe_service_fee_assessments\".\"outcome\" <> 'zero_rounded' OR \"stripe_service_fee_assessments\".\"expected_fee_minor\" = 0" + }, + "stripe_service_fee_assessments_uncharged_outcome_check": { + "name": "stripe_service_fee_assessments_uncharged_outcome_check", + "value": "\"stripe_service_fee_assessments\".\"outcome\" NOT IN ('exempt', 'pre_activation', 'zero_rounded', 'unsupported_currency')\n OR \"stripe_service_fee_assessments\".\"charged_fee_minor\" = 0" + }, + "stripe_service_fee_assessments_exemption_check": { + "name": "stripe_service_fee_assessments_exemption_check", + "value": "(\"stripe_service_fee_assessments\".\"outcome\" = 'exempt') = (\"stripe_service_fee_assessments\".\"exemption_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.stytch_fingerprints": { + "name": "stytch_fingerprints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_fingerprint": { + "name": "visitor_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_fingerprint": { + "name": "browser_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_id": { + "name": "browser_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hardware_fingerprint": { + "name": "hardware_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "network_fingerprint": { + "name": "network_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_id": { + "name": "visitor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verdict_action": { + "name": "verdict_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detected_device_type": { + "name": "detected_device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_authentic_device": { + "name": "is_authentic_device", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{\"\"}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fingerprint_data": { + "name": "fingerprint_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "kilo_free_tier_allowed": { + "name": "kilo_free_tier_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_hardware_fingerprint": { + "name": "idx_hardware_fingerprint", + "columns": [ + { + "expression": "hardware_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_kilo_user_id": { + "name": "idx_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_stytch_fingerprints_reasons_gin": { + "name": "idx_stytch_fingerprints_reasons_gin", + "columns": [ + { + "expression": "reasons", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_verdict_action": { + "name": "idx_verdict_action", + "columns": [ + { + "expression": "verdict_action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_visitor_fingerprint": { + "name": "idx_visitor_fingerprint", + "columns": [ + { + "expression": "visitor_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_prompt_prefix": { + "name": "system_prompt_prefix", + "schema": "", + "columns": { + "system_prompt_prefix_id": { + "name": "system_prompt_prefix_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "system_prompt_prefix": { + "name": "system_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_system_prompt_prefix": { + "name": "UQ_system_prompt_prefix", + "columns": [ + { + "expression": "system_prompt_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.todo_item_sessions": { + "name": "todo_item_sessions", + "schema": "", + "columns": { + "todo_item_id": { + "name": "todo_item_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "linked_at": { + "name": "linked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_todo_item_sessions_todo_item_id": { + "name": "IDX_todo_item_sessions_todo_item_id", + "columns": [ + { + "expression": "todo_item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "todo_item_sessions_todo_item_id_kilo_user_id_fk": { + "name": "todo_item_sessions_todo_item_id_kilo_user_id_fk", + "tableFrom": "todo_item_sessions", + "tableTo": "todo_items", + "columnsFrom": [ + "todo_item_id", + "kilo_user_id" + ], + "columnsTo": [ + "id", + "kilo_user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "todo_item_sessions_session_id_kilo_user_id_fk": { + "name": "todo_item_sessions_session_id_kilo_user_id_fk", + "tableFrom": "todo_item_sessions", + "tableTo": "cli_sessions_v2", + "columnsFrom": [ + "session_id", + "kilo_user_id" + ], + "columnsTo": [ + "session_id", + "kilo_user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "todo_item_sessions_session_id_kilo_user_id_pk": { + "name": "todo_item_sessions_session_id_kilo_user_id_pk", + "columns": [ + "session_id", + "kilo_user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "todo_item_sessions_origin_check": { + "name": "todo_item_sessions_origin_check", + "value": "\"todo_item_sessions\".\"origin\" IN ('started', 'linked')" + } + }, + "isRLSEnabled": false + }, + "public.todo_items": { + "name": "todo_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "list_id": { + "name": "list_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "done_at": { + "name": "done_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "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": { + "IDX_todo_items_list_position": { + "name": "IDX_todo_items_list_position", + "columns": [ + { + "expression": "list_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "todo_items_list_id_kilo_user_id_fk": { + "name": "todo_items_list_id_kilo_user_id_fk", + "tableFrom": "todo_items", + "tableTo": "todo_lists", + "columnsFrom": [ + "list_id", + "kilo_user_id" + ], + "columnsTo": [ + "id", + "kilo_user_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_todo_items_id_kilo_user_id": { + "name": "UQ_todo_items_id_kilo_user_id", + "nullsNotDistinct": false, + "columns": [ + "id", + "kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "todo_items_title_check": { + "name": "todo_items_title_check", + "value": "char_length(btrim(\"todo_items\".\"title\")) BETWEEN 1 AND 500 AND \"todo_items\".\"title\" = btrim(\"todo_items\".\"title\")" + }, + "todo_items_notes_check": { + "name": "todo_items_notes_check", + "value": "char_length(\"todo_items\".\"notes\") <= 10000" + } + }, + "isRLSEnabled": false + }, + "public.todo_lists": { + "name": "todo_lists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_url": { + "name": "git_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "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": { + "IDX_todo_lists_owner_scope_position": { + "name": "IDX_todo_lists_owner_scope_position", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "todo_lists_kilo_user_id_kilocode_users_id_fk": { + "name": "todo_lists_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "todo_lists", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "todo_lists_organization_id_organizations_id_fk": { + "name": "todo_lists_organization_id_organizations_id_fk", + "tableFrom": "todo_lists", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_todo_lists_id_kilo_user_id": { + "name": "UQ_todo_lists_id_kilo_user_id", + "nullsNotDistinct": false, + "columns": [ + "id", + "kilo_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "todo_lists_name_check": { + "name": "todo_lists_name_check", + "value": "char_length(btrim(\"todo_lists\".\"name\")) BETWEEN 1 AND 100 AND \"todo_lists\".\"name\" = btrim(\"todo_lists\".\"name\")" + }, + "todo_lists_emoji_check": { + "name": "todo_lists_emoji_check", + "value": "char_length(\"todo_lists\".\"emoji\") <= 16" + } + }, + "isRLSEnabled": false + }, + "public.transactional_email_log": { + "name": "transactional_email_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_type": { + "name": "email_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_transactional_email_log_type_idempotency_key": { + "name": "UQ_transactional_email_log_type_idempotency_key", + "columns": [ + { + "expression": "email_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_transactional_email_log_user_id": { + "name": "IDX_transactional_email_log_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_transactional_email_log_organization_id": { + "name": "IDX_transactional_email_log_organization_id", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactional_email_log_user_id_kilocode_users_id_fk": { + "name": "transactional_email_log_user_id_kilocode_users_id_fk", + "tableFrom": "transactional_email_log", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "transactional_email_log_organization_id_organizations_id_fk": { + "name": "transactional_email_log_organization_id_organizations_id_fk", + "tableFrom": "transactional_email_log", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "CHK_transactional_email_log_owner": { + "name": "CHK_transactional_email_log_owner", + "value": "\"transactional_email_log\".\"user_id\" IS NOT NULL OR \"transactional_email_log\".\"organization_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.user_activity_tokens": { + "name": "user_activity_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "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()" + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_user_activity_tokens_live_ios_activity": { + "name": "UQ_user_activity_tokens_live_ios_activity", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"organization_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_activity_tokens\".\"kind\" = 'ios_activity' AND \"user_activity_tokens\".\"superseded_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "IDX_user_activity_tokens_user_org": { + "name": "IDX_user_activity_tokens_user_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_activity_tokens_live_session": { + "name": "IDX_user_activity_tokens_live_session", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_activity_tokens\".\"kind\" IN ('ios_session_activity', 'android_session_ongoing') AND \"user_activity_tokens\".\"superseded_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_activity_tokens_user_id_kilocode_users_id_fk": { + "name": "user_activity_tokens_user_id_kilocode_users_id_fk", + "tableFrom": "user_activity_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_activity_tokens_token_session": { + "name": "UQ_user_activity_tokens_token_session", + "nullsNotDistinct": true, + "columns": [ + "token", + "session_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_admin_notes": { + "name": "user_admin_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note_content": { + "name": "note_content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admin_kilo_user_id": { + "name": "admin_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_34517df0b385234babc38fe81b": { + "name": "IDX_34517df0b385234babc38fe81b", + "columns": [ + { + "expression": "admin_kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_ccbde98c4c14046daa5682ec4f": { + "name": "IDX_ccbde98c4c14046daa5682ec4f", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_d0270eb24ef6442d65a0b7853c": { + "name": "IDX_d0270eb24ef6442d65a0b7853c", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_affiliate_attributions": { + "name": "user_affiliate_attributions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tracking_id": { + "name": "tracking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_affiliate_attributions_user_id": { + "name": "IDX_user_affiliate_attributions_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_affiliate_attributions_user_id_kilocode_users_id_fk": { + "name": "user_affiliate_attributions_user_id_kilocode_users_id_fk", + "tableFrom": "user_affiliate_attributions", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_affiliate_attributions_user_provider": { + "name": "UQ_user_affiliate_attributions_user_provider", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_affiliate_attributions_provider_check": { + "name": "user_affiliate_attributions_provider_check", + "value": "\"user_affiliate_attributions\".\"provider\" IN ('impact')" + } + }, + "isRLSEnabled": false + }, + "public.user_affiliate_events": { + "name": "user_affiliate_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_event_id": { + "name": "parent_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "delivery_state": { + "name": "delivery_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stripe_charge_id": { + "name": "stripe_charge_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_action_id": { + "name": "impact_action_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impact_submission_uri": { + "name": "impact_submission_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_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": { + "IDX_user_affiliate_events_claim_path": { + "name": "IDX_user_affiliate_events_claim_path", + "columns": [ + { + "expression": "delivery_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"next_retry_at\", '-infinity'::timestamptz)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_affiliate_events_parent_event_id": { + "name": "IDX_user_affiliate_events_parent_event_id", + "columns": [ + { + "expression": "parent_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_affiliate_events_provider_event_type_charge": { + "name": "IDX_user_affiliate_events_provider_event_type_charge", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stripe_charge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_affiliate_events_user_id_kilocode_users_id_fk": { + "name": "user_affiliate_events_user_id_kilocode_users_id_fk", + "tableFrom": "user_affiliate_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "user_affiliate_events_parent_event_id_fk": { + "name": "user_affiliate_events_parent_event_id_fk", + "tableFrom": "user_affiliate_events", + "tableTo": "user_affiliate_events", + "columnsFrom": [ + "parent_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_affiliate_events_dedupe_key": { + "name": "UQ_user_affiliate_events_dedupe_key", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_affiliate_events_provider_check": { + "name": "user_affiliate_events_provider_check", + "value": "\"user_affiliate_events\".\"provider\" IN ('impact')" + }, + "user_affiliate_events_event_type_check": { + "name": "user_affiliate_events_event_type_check", + "value": "\"user_affiliate_events\".\"event_type\" IN ('signup', 'trial_start', 'trial_end', 'sale', 'sale_reversal')" + }, + "user_affiliate_events_delivery_state_check": { + "name": "user_affiliate_events_delivery_state_check", + "value": "\"user_affiliate_events\".\"delivery_state\" IN ('queued', 'blocked', 'sending', 'delivered', 'failed')" + }, + "user_affiliate_events_attempt_count_non_negative_check": { + "name": "user_affiliate_events_attempt_count_non_negative_check", + "value": "\"user_affiliate_events\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_auth_provider": { + "name": "user_auth_provider", + "schema": "", + "columns": { + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hosted_domain": { + "name": "hosted_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_auth_provider_kilo_user_id": { + "name": "IDX_user_auth_provider_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_auth_provider_hosted_domain": { + "name": "IDX_user_auth_provider_hosted_domain", + "columns": [ + { + "expression": "hosted_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_auth_provider_lower_email": { + "name": "IDX_user_auth_provider_lower_email", + "columns": [ + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_auth_provider_provider_provider_account_id_pk": { + "name": "user_auth_provider_provider_provider_account_id_pk", + "columns": [ + "provider", + "provider_account_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_data_export_object_deletions": { + "name": "user_data_export_object_deletions", + "schema": "", + "columns": { + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'account_deletion'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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": { + "IDX_user_data_export_object_deletions_ready": { + "name": "IDX_user_data_export_object_deletions_ready", + "columns": [ + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_data_export_object_deletions_reason_check": { + "name": "user_data_export_object_deletions_reason_check", + "value": "\"user_data_export_object_deletions\".\"reason\" IN ('account_deletion', 'admin_cancel', 'admin_replace')" + }, + "user_data_export_object_deletions_attempt_count_nonnegative": { + "name": "user_data_export_object_deletions_attempt_count_nonnegative", + "value": "\"user_data_export_object_deletions\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_data_export_outbox": { + "name": "user_data_export_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "export_id": { + "name": "export_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'generate'" + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sent_at": { + "name": "sent_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": { + "IDX_user_data_export_outbox_pending": { + "name": "IDX_user_data_export_outbox_pending", + "columns": [ + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_export_outbox\".\"sent_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_data_export_outbox_export_id_user_data_exports_id_fk": { + "name": "user_data_export_outbox_export_id_user_data_exports_id_fk", + "tableFrom": "user_data_export_outbox", + "tableTo": "user_data_exports", + "columnsFrom": [ + "export_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_data_export_outbox_generation_operation": { + "name": "UQ_user_data_export_outbox_generation_operation", + "nullsNotDistinct": false, + "columns": [ + "export_id", + "generation", + "operation" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_data_export_outbox_operation_check": { + "name": "user_data_export_outbox_operation_check", + "value": "\"user_data_export_outbox\".\"operation\" = 'generate'" + }, + "user_data_export_outbox_generation_nonnegative": { + "name": "user_data_export_outbox_generation_nonnegative", + "value": "\"user_data_export_outbox\".\"generation\" >= 0" + }, + "user_data_export_outbox_attempt_count_nonnegative": { + "name": "user_data_export_outbox_attempt_count_nonnegative", + "value": "\"user_data_export_outbox\".\"attempt_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_data_export_parts": { + "name": "user_data_export_parts", + "schema": "", + "columns": { + "export_id": { + "name": "export_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "part_number": { + "name": "part_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_data_export_parts_export_id_user_data_exports_id_fk": { + "name": "user_data_export_parts_export_id_user_data_exports_id_fk", + "tableFrom": "user_data_export_parts", + "tableTo": "user_data_exports", + "columnsFrom": [ + "export_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_data_export_parts_export_id_part_number_pk": { + "name": "user_data_export_parts_export_id_part_number_pk", + "columns": [ + "export_id", + "part_number" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_data_export_parts_part_number_positive": { + "name": "user_data_export_parts_part_number_positive", + "value": "\"user_data_export_parts\".\"part_number\" > 0" + }, + "user_data_export_parts_size_bytes_nonnegative": { + "name": "user_data_export_parts_size_bytes_nonnegative", + "value": "\"user_data_export_parts\".\"size_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.user_data_exports": { + "name": "user_data_exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "snapshot_at": { + "name": "snapshot_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "current_source": { + "name": "current_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_cursor": { + "name": "source_cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "multipart_upload_id": { + "name": "multipart_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_part_number": { + "name": "next_part_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "dispatch_generation": { + "name": "dispatch_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_token": { + "name": "lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "row_count": { + "name": "row_count", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "r2_object_key": { + "name": "r2_object_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "r2_etag": { + "name": "r2_etag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_redacted": { + "name": "last_error_redacted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_status": { + "name": "email_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "email_attempt_count": { + "name": "email_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "email_lease_token": { + "name": "email_lease_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "email_lease_expires_at": { + "name": "email_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_sent_at": { + "name": "email_sent_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": { + "UQ_user_data_exports_single_active": { + "name": "UQ_user_data_exports_single_active", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_data_exports\".\"status\" IN ('queued', 'processing', 'finalizing') AND \"user_data_exports\".\"subject_type\" = 'user'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_data_exports_single_active_org": { + "name": "UQ_user_data_exports_single_active_org", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_data_exports\".\"status\" IN ('queued', 'processing', 'finalizing') AND \"user_data_exports\".\"subject_type\" = 'organization'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_user_created": { + "name": "IDX_user_data_exports_user_created", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_org_created": { + "name": "IDX_user_data_exports_org_created", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_lease_expiry": { + "name": "IDX_user_data_exports_lease_expiry", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"status\" IN ('processing', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_ready_expiry": { + "name": "IDX_user_data_exports_ready_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"status\" = 'ready'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_failed_multipart": { + "name": "IDX_user_data_exports_failed_multipart", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"status\" = 'failed' AND \"user_data_exports\".\"multipart_upload_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_data_exports_email_lease_expiry": { + "name": "IDX_user_data_exports_email_lease_expiry", + "columns": [ + { + "expression": "email_lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_data_exports\".\"email_status\" = 'sending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_data_exports_kilo_user_id_kilocode_users_id_fk": { + "name": "user_data_exports_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_data_exports", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "user_data_exports_organization_id_organizations_id_fk": { + "name": "user_data_exports_organization_id_organizations_id_fk", + "tableFrom": "user_data_exports", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_data_exports_status_check": { + "name": "user_data_exports_status_check", + "value": "\"user_data_exports\".\"status\" IN ('queued', 'processing', 'finalizing', 'ready', 'failed', 'expired')" + }, + "user_data_exports_subject_type_check": { + "name": "user_data_exports_subject_type_check", + "value": "\"user_data_exports\".\"subject_type\" IN ('user', 'organization')" + }, + "user_data_exports_subject_shape": { + "name": "user_data_exports_subject_shape", + "value": "(\"user_data_exports\".\"subject_type\" = 'user' AND \"user_data_exports\".\"organization_id\" IS NULL)\n OR (\"user_data_exports\".\"subject_type\" = 'organization' AND \"user_data_exports\".\"organization_id\" IS NOT NULL)" + }, + "user_data_exports_schema_version_positive": { + "name": "user_data_exports_schema_version_positive", + "value": "\"user_data_exports\".\"schema_version\" > 0" + }, + "user_data_exports_next_part_number_positive": { + "name": "user_data_exports_next_part_number_positive", + "value": "\"user_data_exports\".\"next_part_number\" > 0" + }, + "user_data_exports_dispatch_generation_nonnegative": { + "name": "user_data_exports_dispatch_generation_nonnegative", + "value": "\"user_data_exports\".\"dispatch_generation\" >= 0" + }, + "user_data_exports_attempt_count_nonnegative": { + "name": "user_data_exports_attempt_count_nonnegative", + "value": "\"user_data_exports\".\"attempt_count\" >= 0" + }, + "user_data_exports_row_count_nonnegative": { + "name": "user_data_exports_row_count_nonnegative", + "value": "\"user_data_exports\".\"row_count\" >= 0" + }, + "user_data_exports_size_bytes_nonnegative": { + "name": "user_data_exports_size_bytes_nonnegative", + "value": "\"user_data_exports\".\"size_bytes\" IS NULL OR \"user_data_exports\".\"size_bytes\" >= 0" + }, + "user_data_exports_lease_shape": { + "name": "user_data_exports_lease_shape", + "value": "(\"user_data_exports\".\"lease_token\" IS NULL) = (\"user_data_exports\".\"lease_expires_at\" IS NULL)" + }, + "user_data_exports_ready_shape": { + "name": "user_data_exports_ready_shape", + "value": "\"user_data_exports\".\"status\" <> 'ready' OR (\"user_data_exports\".\"r2_object_key\" IS NOT NULL AND \"user_data_exports\".\"size_bytes\" IS NOT NULL AND \"user_data_exports\".\"completed_at\" IS NOT NULL AND \"user_data_exports\".\"expires_at\" IS NOT NULL)" + }, + "user_data_exports_last_error_redacted_length": { + "name": "user_data_exports_last_error_redacted_length", + "value": "\"user_data_exports\".\"last_error_redacted\" IS NULL OR length(\"user_data_exports\".\"last_error_redacted\") <= 500" + }, + "user_data_exports_email_attempt_count_nonnegative": { + "name": "user_data_exports_email_attempt_count_nonnegative", + "value": "\"user_data_exports\".\"email_attempt_count\" >= 0" + }, + "user_data_exports_email_status_check": { + "name": "user_data_exports_email_status_check", + "value": "\"user_data_exports\".\"email_status\" IN ('pending', 'sending', 'sent', 'failed')" + }, + "user_data_exports_email_lease_shape": { + "name": "user_data_exports_email_lease_shape", + "value": "(\"user_data_exports\".\"email_status\" = 'sending') = (\"user_data_exports\".\"email_lease_token\" IS NOT NULL AND \"user_data_exports\".\"email_lease_expires_at\" IS NOT NULL)" + }, + "user_data_exports_email_sent_shape": { + "name": "user_data_exports_email_sent_shape", + "value": "(\"user_data_exports\".\"email_status\" = 'sent') = (\"user_data_exports\".\"email_sent_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.user_deletion_activity": { + "name": "user_deletion_activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step_key": { + "name": "step_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details_json": { + "name": "details_json", + "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": { + "IDX_user_deletion_activity_request_created": { + "name": "IDX_user_deletion_activity_request_created", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_deletion_activity_request_id_user_deletion_requests_id_fk": { + "name": "user_deletion_activity_request_id_user_deletion_requests_id_fk", + "tableFrom": "user_deletion_activity", + "tableTo": "user_deletion_requests", + "columnsFrom": [ + "request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_deletion_audit_events": { + "name": "user_deletion_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_kilo_user_id": { + "name": "actor_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_email_hmac": { + "name": "target_email_hmac", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_key": { + "name": "subject_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details_json": { + "name": "details_json", + "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": { + "UQ_user_deletion_audit_events_idempotent": { + "name": "UQ_user_deletion_audit_events_idempotent", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_deletion_audit_events\".\"request_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_audit_events_request_id": { + "name": "IDX_user_deletion_audit_events_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_audit_events_hmac": { + "name": "IDX_user_deletion_audit_events_hmac", + "columns": [ + { + "expression": "target_email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_deletion_audit_events_request_id_user_deletion_requests_id_fk": { + "name": "user_deletion_audit_events_request_id_user_deletion_requests_id_fk", + "tableFrom": "user_deletion_audit_events", + "tableTo": "user_deletion_requests", + "columnsFrom": [ + "request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_deletion_audit_events_event_type_check": { + "name": "user_deletion_audit_events_event_type_check", + "value": "\"user_deletion_audit_events\".\"event_type\" IN ('request_created', 'intake_refused', 'access_disabled', 'access_absent', 'preflight_disposition', 'task_disposition', 'manual_retry', 'manual_action', 'anonymized', 'deletion_ready_for_customer_reply', 'cancelled', 'completed')" + } + }, + "isRLSEnabled": false + }, + "public.user_deletion_provider_credentials": { + "name": "user_deletion_provider_credentials", + "schema": "", + "columns": { + "provider_scope": { + "name": "provider_scope", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "encrypted_material": { + "name": "encrypted_material", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by_kilo_user_id": { + "name": "updated_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_deletion_provider_credentials_updated_by_kilo_user_id_kilocode_users_id_fk": { + "name": "user_deletion_provider_credentials_updated_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_deletion_provider_credentials", + "tableTo": "kilocode_users", + "columnsFrom": [ + "updated_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_deletion_provider_credentials_scope_check": { + "name": "user_deletion_provider_credentials_scope_check", + "value": "\"user_deletion_provider_credentials\".\"provider_scope\" IN ('kiloclaw', 'customerio', 'cloud_storage', 'session_ingest', 'posthog', 'substack', 'pylon', 'csa')" + } + }, + "isRLSEnabled": false + }, + "public.user_deletion_requests": { + "name": "user_deletion_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "catalog_version": { + "name": "catalog_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "requested_by_kilo_user_id": { + "name": "requested_by_kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_email": { + "name": "requested_by_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_email_hmac": { + "name": "target_email_hmac", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pylon_ticket_ref": { + "name": "pylon_ticket_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_subject_resolution": { + "name": "cloud_subject_resolution", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cloud_subject_proof_ref": { + "name": "cloud_subject_proof_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preflight_attention_code": { + "name": "preflight_attention_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_progress_at": { + "name": "last_progress_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "anonymized_at": { + "name": "anonymized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "UQ_user_deletion_requests_active_email_hmac": { + "name": "UQ_user_deletion_requests_active_email_hmac", + "columns": [ + { + "expression": "target_email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_deletion_requests\".\"target_email_hmac\" IS NOT NULL AND \"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_deletion_requests_active_user_id": { + "name": "UQ_user_deletion_requests_active_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_deletion_requests\".\"user_id\" IS NOT NULL AND \"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_deletion_requests_active_pylon_ticket": { + "name": "UQ_user_deletion_requests_active_pylon_ticket", + "columns": [ + { + "expression": "regexp_replace(\"pylon_ticket_ref\", '^#', '')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_deletion_requests\".\"pylon_ticket_ref\" IS NOT NULL AND \"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_requests_fairness": { + "name": "IDX_user_deletion_requests_fairness", + "columns": [ + { + "expression": "last_progress_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_requests_email_hmac": { + "name": "IDX_user_deletion_requests_email_hmac", + "columns": [ + { + "expression": "target_email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_requests_user_id": { + "name": "IDX_user_deletion_requests_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_deletion_requests_user_id_kilocode_users_id_fk": { + "name": "user_deletion_requests_user_id_kilocode_users_id_fk", + "tableFrom": "user_deletion_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_deletion_requests_requested_by_kilo_user_id_kilocode_users_id_fk": { + "name": "user_deletion_requests_requested_by_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_deletion_requests", + "tableTo": "kilocode_users", + "columnsFrom": [ + "requested_by_kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_deletion_requests_status_check": { + "name": "user_deletion_requests_status_check", + "value": "\"user_deletion_requests\".\"status\" IN ('pending', 'in_progress', 'finalizing', 'completed', 'cancelled')" + }, + "user_deletion_requests_cloud_subject_resolution_check": { + "name": "user_deletion_requests_cloud_subject_resolution_check", + "value": "\"user_deletion_requests\".\"cloud_subject_resolution\" IN ('current_user', 'authoritative_absence', 'prior_queue_cleanup', 'legacy_identity_unresolved', 'unresolved')" + }, + "user_deletion_requests_catalog_version_positive": { + "name": "user_deletion_requests_catalog_version_positive", + "value": "\"user_deletion_requests\".\"catalog_version\" >= 1" + }, + "user_deletion_requests_completed_at_check": { + "name": "user_deletion_requests_completed_at_check", + "value": "(\"user_deletion_requests\".\"status\" = 'completed') = (\"user_deletion_requests\".\"completed_at\" IS NOT NULL)" + }, + "user_deletion_requests_cancelled_at_check": { + "name": "user_deletion_requests_cancelled_at_check", + "value": "(\"user_deletion_requests\".\"status\" = 'cancelled') = (\"user_deletion_requests\".\"cancelled_at\" IS NOT NULL)" + }, + "user_deletion_requests_active_email_check": { + "name": "user_deletion_requests_active_email_check", + "value": "(\"user_deletion_requests\".\"status\" NOT IN ('in_progress', 'finalizing') OR \"user_deletion_requests\".\"target_email\" IS NOT NULL) AND (\"user_deletion_requests\".\"status\" NOT IN ('completed', 'cancelled') OR \"user_deletion_requests\".\"target_email\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.user_deletion_steps": { + "name": "user_deletion_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "request_id": { + "name": "request_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "step_key": { + "name": "step_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claim_token": { + "name": "claim_token", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "claimed_until": { + "name": "claimed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "window_attempt_count": { + "name": "window_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lifetime_attempt_count": { + "name": "lifetime_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "progress_json": { + "name": "progress_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rate_limited_since": { + "name": "rate_limited_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "manual_evidence_json": { + "name": "manual_evidence_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_user_deletion_steps_due": { + "name": "IDX_user_deletion_steps_due", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_deletion_steps\".\"status\" IN ('pending', 'retry_wait', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_deletion_steps_request_id": { + "name": "IDX_user_deletion_steps_request_id", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_deletion_steps_request_id_user_deletion_requests_id_fk": { + "name": "user_deletion_steps_request_id_user_deletion_requests_id_fk", + "tableFrom": "user_deletion_steps", + "tableTo": "user_deletion_requests", + "columnsFrom": [ + "request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_user_deletion_steps_request_step": { + "name": "UQ_user_deletion_steps_request_step", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "step_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "user_deletion_steps_step_key_check": { + "name": "user_deletion_steps_step_key_check", + "value": "\"user_deletion_steps\".\"step_key\" IN ('kiloclaw_destroy', 'customerio', 'cli_v1_blobs', 'cli_v2_sessions', 'usage_prompt_prefixes', 'posthog', 'substack', 'anonymize', 'pylon_reply', 'pylon_finalize', 'completion_email', 'pylon_contact', 'csa_support_db')" + }, + "user_deletion_steps_status_check": { + "name": "user_deletion_steps_status_check", + "value": "\"user_deletion_steps\".\"status\" IN ('pending', 'running', 'retry_wait', 'needs_attention', 'manual_action_required', 'succeeded', 'not_applicable', 'manually_verified')" + }, + "user_deletion_steps_window_attempt_count_nonnegative": { + "name": "user_deletion_steps_window_attempt_count_nonnegative", + "value": "\"user_deletion_steps\".\"window_attempt_count\" >= 0" + }, + "user_deletion_steps_lifetime_attempt_count_nonnegative": { + "name": "user_deletion_steps_lifetime_attempt_count_nonnegative", + "value": "\"user_deletion_steps\".\"lifetime_attempt_count\" >= 0" + }, + "user_deletion_steps_claim_fields_check": { + "name": "user_deletion_steps_claim_fields_check", + "value": "(\"user_deletion_steps\".\"claim_token\" IS NULL) = (\"user_deletion_steps\".\"claimed_until\" IS NULL)" + }, + "user_deletion_steps_manual_evidence_check": { + "name": "user_deletion_steps_manual_evidence_check", + "value": "(\"user_deletion_steps\".\"status\" = 'manually_verified') = (\"user_deletion_steps\".\"manual_evidence_json\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.user_feedback": { + "name": "user_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feedback_for": { + "name": "feedback_for", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "feedback_batch": { + "name": "feedback_batch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "context_json": { + "name": "context_json", + "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": { + "IDX_user_feedback_created_at": { + "name": "IDX_user_feedback_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_kilo_user_id": { + "name": "IDX_user_feedback_kilo_user_id", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_feedback_for": { + "name": "IDX_user_feedback_feedback_for", + "columns": [ + { + "expression": "feedback_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_feedback_batch": { + "name": "IDX_user_feedback_feedback_batch", + "columns": [ + { + "expression": "feedback_batch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_feedback_source": { + "name": "IDX_user_feedback_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_feedback_kilo_user_id_kilocode_users_id_fk": { + "name": "user_feedback_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_feedback", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_github_app_tokens": { + "name": "user_github_app_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_app_type": { + "name": "github_app_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "github_user_id": { + "name": "github_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "credential_version": { + "name": "credential_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revocation_reason": { + "name": "revocation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_github_app_tokens_user_app": { + "name": "UQ_user_github_app_tokens_user_app", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UQ_user_github_app_tokens_github_user_app": { + "name": "UQ_user_github_app_tokens_github_user_app", + "columns": [ + { + "expression": "github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_app_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_github_app_tokens_kilo_user_id_kilocode_users_id_fk": { + "name": "user_github_app_tokens_kilo_user_id_kilocode_users_id_fk", + "tableFrom": "user_github_app_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "kilo_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_github_app_tokens_app_type_check": { + "name": "user_github_app_tokens_app_type_check", + "value": "\"user_github_app_tokens\".\"github_app_type\" IN ('standard', 'lite')" + } + }, + "isRLSEnabled": false + }, + "public.user_model_preferences": { + "name": "user_model_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "favorites": { + "name": "favorites", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "last_selected": { + "name": "last_selected", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_model_preferences_user_id": { + "name": "UQ_user_model_preferences_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_model_preferences_user_id_kilocode_users_id_fk": { + "name": "user_model_preferences_user_id_kilocode_users_id_fk", + "tableFrom": "user_model_preferences", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_moderation_blocks": { + "name": "user_moderation_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "blocker_user_id": { + "name": "blocker_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blocked_github_login": { + "name": "blocked_github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_moderation_blocks_blocker_login": { + "name": "UQ_user_moderation_blocks_blocker_login", + "columns": [ + { + "expression": "blocker_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "blocked_github_login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_moderation_mutes": { + "name": "user_moderation_mutes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "blocker_user_id": { + "name": "blocker_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "muted_github_login": { + "name": "muted_github_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_moderation_mutes_blocker_login": { + "name": "UQ_user_moderation_mutes_blocker_login", + "columns": [ + { + "expression": "blocker_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "muted_github_login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_notification_history": { + "name": "user_notification_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "push_type": { + "name": "push_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "i18n_key": { + "name": "i18n_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "i18n_params": { + "name": "i18n_params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "fallback_title": { + "name": "fallback_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fallback_body": { + "name": "fallback_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_bucket": { + "name": "dedupe_bucket", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_notification_history_user_idempotency": { + "name": "UQ_user_notification_history_user_idempotency", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_bucket", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_notification_history_user_created": { + "name": "IDX_user_notification_history_user_created", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_notification_history_user_category_created": { + "name": "IDX_user_notification_history_user_category_created", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_notification_history_created_at": { + "name": "IDX_user_notification_history_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_notification_history_user_id_kilocode_users_id_fk": { + "name": "user_notification_history_user_id_kilocode_users_id_fk", + "tableFrom": "user_notification_history", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_notification_preferences": { + "name": "user_notification_preferences", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_push_enabled": { + "name": "agent_push_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "chat_messages_enabled": { + "name": "chat_messages_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "agent_attention_enabled": { + "name": "agent_attention_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "session_status_enabled": { + "name": "session_status_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "kiloclaw_activity_enabled": { + "name": "kiloclaw_activity_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "balance_alerts_enabled": { + "name": "balance_alerts_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "spend_alerts_enabled": { + "name": "spend_alerts_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "security_findings_enabled": { + "name": "security_findings_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notification_previews": { + "name": "notification_previews", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'generic'" + }, + "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": { + "user_notification_preferences_user_id_kilocode_users_id_fk": { + "name": "user_notification_preferences_user_id_kilocode_users_id_fk", + "tableFrom": "user_notification_preferences", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_push_tokens": { + "name": "user_push_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_version": { + "name": "app_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locale": { + "name": "locale", + "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": { + "UQ_user_push_tokens_token": { + "name": "UQ_user_push_tokens_token", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_user_push_tokens_user_id": { + "name": "IDX_user_push_tokens_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_push_tokens_user_id_kilocode_users_id_fk": { + "name": "user_push_tokens_user_id_kilocode_users_id_fk", + "tableFrom": "user_push_tokens", + "tableTo": "kilocode_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_terms_acceptances": { + "name": "user_terms_acceptances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "terms_version": { + "name": "terms_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "age_posture": { + "name": "age_posture", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'13_plus'" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "UQ_user_terms_acceptances_user_version": { + "name": "UQ_user_terms_acceptances_user_version", + "columns": [ + { + "expression": "kilo_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "terms_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_ip_city": { + "name": "vercel_ip_city", + "schema": "", + "columns": { + "vercel_ip_city_id": { + "name": "vercel_ip_city_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vercel_ip_city": { + "name": "vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_vercel_ip_city": { + "name": "UQ_vercel_ip_city", + "columns": [ + { + "expression": "vercel_ip_city", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_ip_country": { + "name": "vercel_ip_country", + "schema": "", + "columns": { + "vercel_ip_country_id": { + "name": "vercel_ip_country_id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "vercel_ip_country": { + "name": "vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UQ_vercel_ip_country": { + "name": "UQ_vercel_ip_country", + "columns": [ + { + "expression": "vercel_ip_country", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_events": { + "name": "webhook_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "pg_catalog.gen_random_uuid()" + }, + "owned_by_organization_id": { + "name": "owned_by_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owned_by_user_id": { + "name": "owned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_action": { + "name": "event_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "processed": { + "name": "processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "handlers_triggered": { + "name": "handlers_triggered", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "event_signature": { + "name": "event_signature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "IDX_webhook_events_owned_by_org_id": { + "name": "IDX_webhook_events_owned_by_org_id", + "columns": [ + { + "expression": "owned_by_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_owned_by_user_id": { + "name": "IDX_webhook_events_owned_by_user_id", + "columns": [ + { + "expression": "owned_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_platform": { + "name": "IDX_webhook_events_platform", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_event_type": { + "name": "IDX_webhook_events_event_type", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "IDX_webhook_events_created_at": { + "name": "IDX_webhook_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_events_owned_by_organization_id_organizations_id_fk": { + "name": "webhook_events_owned_by_organization_id_organizations_id_fk", + "tableFrom": "webhook_events", + "tableTo": "organizations", + "columnsFrom": [ + "owned_by_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_events_owned_by_user_id_kilocode_users_id_fk": { + "name": "webhook_events_owned_by_user_id_kilocode_users_id_fk", + "tableFrom": "webhook_events", + "tableTo": "kilocode_users", + "columnsFrom": [ + "owned_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "UQ_webhook_events_signature": { + "name": "UQ_webhook_events_signature", + "nullsNotDistinct": false, + "columns": [ + "event_signature" + ] + } + }, + "policies": {}, + "checkConstraints": { + "webhook_events_owner_check": { + "name": "webhook_events_owner_check", + "value": "(\n (\"webhook_events\".\"owned_by_user_id\" IS NOT NULL AND \"webhook_events\".\"owned_by_organization_id\" IS NULL) OR\n (\"webhook_events\".\"owned_by_user_id\" IS NULL AND \"webhook_events\".\"owned_by_organization_id\" IS NOT NULL)\n )" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.microdollar_usage_view": { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kilo_user_id": { + "name": "kilo_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "output_tokens": { + "name": "output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_write_tokens": { + "name": "cache_write_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cache_hit_tokens": { + "name": "cache_hit_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "http_x_forwarded_for": { + "name": "http_x_forwarded_for", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_city": { + "name": "http_x_vercel_ip_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_country": { + "name": "http_x_vercel_ip_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_latitude": { + "name": "http_x_vercel_ip_latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ip_longitude": { + "name": "http_x_vercel_ip_longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "http_x_vercel_ja4_digest": { + "name": "http_x_vercel_ja4_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_model": { + "name": "requested_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_prompt_prefix": { + "name": "user_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_prefix": { + "name": "system_prompt_prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_prompt_length": { + "name": "system_prompt_length", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "http_user_agent": { + "name": "http_user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cache_discount": { + "name": "cache_discount", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "has_middle_out_transform": { + "name": "has_middle_out_transform", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "abuse_classification": { + "name": "abuse_classification", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inference_provider": { + "name": "inference_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "upstream_id": { + "name": "upstream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finish_reason": { + "name": "finish_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latency": { + "name": "latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "moderation_latency": { + "name": "moderation_latency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "generation_time": { + "name": "generation_time", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_byok": { + "name": "is_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_user_byok": { + "name": "is_user_byok", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "streamed": { + "name": "streamed", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancelled": { + "name": "cancelled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "editor_name": { + "name": "editor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_kind": { + "name": "api_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_tools": { + "name": "has_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature": { + "name": "feature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_model": { + "name": "auto_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reasoning_setting": { + "name": "reasoning_setting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "market_cost": { + "name": "market_cost", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "is_free": { + "name": "is_free", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "abuse_delay": { + "name": "abuse_delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "abuse_downgraded_from": { + "name": "abuse_downgraded_from", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "definition": "\n SELECT\n mu.id,\n mu.kilo_user_id,\n meta.message_id,\n mu.cost,\n mu.input_tokens,\n mu.output_tokens,\n mu.cache_write_tokens,\n mu.cache_hit_tokens,\n mu.created_at,\n ip.http_ip AS http_x_forwarded_for,\n city.vercel_ip_city AS http_x_vercel_ip_city,\n country.vercel_ip_country AS http_x_vercel_ip_country,\n meta.vercel_ip_latitude AS http_x_vercel_ip_latitude,\n meta.vercel_ip_longitude AS http_x_vercel_ip_longitude,\n ja4.ja4_digest AS http_x_vercel_ja4_digest,\n mu.provider,\n mu.model,\n mu.requested_model,\n meta.user_prompt_prefix,\n spp.system_prompt_prefix,\n meta.system_prompt_length,\n ua.http_user_agent,\n mu.cache_discount,\n meta.max_tokens,\n meta.has_middle_out_transform,\n mu.has_error,\n mu.abuse_classification,\n mu.organization_id,\n mu.inference_provider,\n mu.project_id,\n meta.status_code,\n meta.upstream_id,\n frfr.finish_reason,\n meta.latency,\n meta.moderation_latency,\n meta.generation_time,\n meta.is_byok,\n meta.is_user_byok,\n meta.streamed,\n meta.cancelled,\n edit.editor_name,\n ak.api_kind,\n meta.has_tools,\n meta.machine_id,\n feat.feature,\n meta.session_id,\n md.mode,\n am.auto_model,\n rs.reasoning_setting,\n meta.market_cost,\n meta.is_free,\n meta.abuse_delay,\n meta.abuse_downgraded_from\n FROM \"microdollar_usage\" mu\n LEFT JOIN \"microdollar_usage_metadata\" meta ON mu.id = meta.id\n LEFT JOIN \"http_ip\" ip ON meta.http_ip_id = ip.http_ip_id\n LEFT JOIN \"vercel_ip_city\" city ON meta.vercel_ip_city_id = city.vercel_ip_city_id\n LEFT JOIN \"vercel_ip_country\" country ON meta.vercel_ip_country_id = country.vercel_ip_country_id\n LEFT JOIN \"ja4_digest\" ja4 ON meta.ja4_digest_id = ja4.ja4_digest_id\n LEFT JOIN \"system_prompt_prefix\" spp ON meta.system_prompt_prefix_id = spp.system_prompt_prefix_id\n LEFT JOIN \"http_user_agent\" ua ON meta.http_user_agent_id = ua.http_user_agent_id\n LEFT JOIN \"finish_reason\" frfr ON meta.finish_reason_id = frfr.finish_reason_id\n LEFT JOIN \"editor_name\" edit ON meta.editor_name_id = edit.editor_name_id\n LEFT JOIN \"api_kind\" ak ON meta.api_kind_id = ak.api_kind_id\n LEFT JOIN \"feature\" feat ON meta.feature_id = feat.feature_id\n LEFT JOIN \"mode\" md ON meta.mode_id = md.mode_id\n LEFT JOIN \"auto_model\" am ON meta.auto_model_id = am.auto_model_id\n LEFT JOIN \"reasoning_setting\" rs ON meta.reasoning_setting_id = rs.reasoning_setting_id\n", + "name": "microdollar_usage_view", + "schema": "public", + "isExisting": false, + "materialized": false + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index f92221e5021..8a0bb7b1900 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -2115,6 +2115,13 @@ "when": 1792876471766, "tag": "0301_contributor_credit_window", "breakpoints": true + }, + { + "idx": 302, + "version": "7", + "when": 1792880301492, + "tag": "0302_typical_squirrel_girl", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema-types.ts b/packages/db/src/schema-types.ts index f23578b4b85..188c443636a 100644 --- a/packages/db/src/schema-types.ts +++ b/packages/db/src/schema-types.ts @@ -1300,6 +1300,43 @@ export type EncryptedData = { authTag: string; }; +/** + * RSA + AES envelope stored for secrets that are encrypted by the web app + * and decrypted by a Worker. Kept structural here so the schema package does + * not need to depend on the runtime encryption implementation. + */ +export type VercelComputeCredentialEnvelope = { + scheme: 'byoc-vercel-credential-rsa-aes-256-gcm'; + version: 1; + keyId: string; + ciphertext: { + encryptedData: string; + encryptedDEK: string; + algorithm: 'rsa-aes-256-gcm'; + version: 1; + }; +}; + +export type VercelComputeSetupStatus = 'pending' | 'building' | 'ready' | 'failed'; + +export type VercelComputeUpgradeStatus = 'pending' | 'building' | 'failed'; + +export type VercelComputeTokenScope = 'team' | 'project'; + +export type VercelComputeSetupStep = + | 'validating_access' + | 'create_builder' + | 'install_system_dependencies' + | 'install_node_dependencies' + | 'upload_runtime_artifacts' + | 'verify_runtime_artifacts' + | 'snapshot_builder' + | 'create_validator' + | 'launch_validator_wrapper' + | 'verify_validator_call_home' + | 'stop_validator' + | 'confirm_terminal'; + // --- AuthProviderId --- export type AuthProviderId = diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index b2f32f9b595..46d4bc8d25f 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -146,6 +146,11 @@ import type { OrganizationSettings, AuditLogAction, EncryptedData, + VercelComputeCredentialEnvelope, + VercelComputeSetupStatus, + VercelComputeTokenScope, + VercelComputeSetupStep, + VercelComputeUpgradeStatus, AuthProviderId, PlatformRepository, IntegrationPermissions, @@ -3801,6 +3806,82 @@ export const organization_memberships = pgTable( export type OrganizationMembership = typeof organization_memberships.$inferSelect; +export const organization_vercel_compute_credentials = pgTable( + 'organization_vercel_compute_credentials', + { + id: idPrimaryKeyColumn, + organization_id: uuid().references(() => organizations.id, { + onDelete: 'cascade', + onUpdate: 'cascade', + }), + user_id: text(), + token_encrypted: jsonb().$type().notNull(), + token_scope: text().$type().notNull().default('team'), + team_id: text().notNull(), + project_id: text().notNull(), + team_slug: text(), + project_slug: text(), + setup_status: text().$type().notNull().default('pending'), + setup_step: text().$type(), + setup_error: text(), + build_generation: uuid() + .notNull() + .default(sql`pg_catalog.gen_random_uuid()`), + runtime_build_id: text(), + runtime_snapshot_id: text(), + runtime_wrapper_version: text(), + runtime_released_at: text(), + runtime_digest: text(), + upgrade_status: text().$type(), + upgrade_step: text().$type(), + upgrade_error: text(), + setup_started_at: timestamp({ withTimezone: true, mode: 'string' }), + setup_completed_at: timestamp({ withTimezone: true, mode: 'string' }), + created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updated_at: timestamp({ withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull() + .$onUpdateFn(() => sql`now()`), + }, + table => [ + uniqueIndex('UQ_organization_vercel_compute_credentials_organization') + .on(table.organization_id) + .where(sql`${table.organization_id} IS NOT NULL`), + uniqueIndex('UQ_organization_vercel_compute_credentials_user') + .on(table.user_id) + .where(sql`${table.user_id} IS NOT NULL`), + check( + 'organization_vercel_compute_credentials_owner_check', + sql`(${table.organization_id} IS NOT NULL AND ${table.user_id} IS NULL) OR (${table.organization_id} IS NULL AND ${table.user_id} IS NOT NULL)` + ), + check( + 'organization_vercel_compute_credentials_ids_non_empty', + sql`length(trim(${table.team_id})) > 0 AND length(trim(${table.project_id})) > 0` + ), + check( + 'organization_vercel_compute_credentials_token_scope_check', + sql`${table.token_scope} IN ('team', 'project')` + ), + check( + 'organization_vercel_compute_credentials_status_check', + sql`${table.setup_status} IN ('pending', 'building', 'ready', 'failed')` + ), + check( + 'organization_vercel_compute_credentials_ready_fields_check', + sql`${table.setup_status} <> 'ready' OR (${table.runtime_snapshot_id} IS NOT NULL AND ${table.project_slug} IS NOT NULL AND ${table.setup_completed_at} IS NOT NULL)` + ), + check( + 'organization_vercel_compute_credentials_upgrade_status_check', + sql`${table.upgrade_status} IS NULL OR ${table.upgrade_status} IN ('pending', 'building', 'failed')` + ), + ] +); + +export type OrganizationVercelComputeCredential = + typeof organization_vercel_compute_credentials.$inferSelect; +export type NewOrganizationVercelComputeCredential = + typeof organization_vercel_compute_credentials.$inferInsert; + export const organization_groups = pgTable( 'organization_groups', { diff --git a/packages/worker-utils/src/sandbox-allocation.test.ts b/packages/worker-utils/src/sandbox-allocation.test.ts index 836c450d203..dab87de6c8f 100644 --- a/packages/worker-utils/src/sandbox-allocation.test.ts +++ b/packages/worker-utils/src/sandbox-allocation.test.ts @@ -8,8 +8,7 @@ import { getSandboxAllocationProvider, getSandboxAllocationResources, getSandboxAllocationRequest, - getSandboxAllocationKey, - getKiloSandboxAllocation, + getSandboxAllocation, sandboxAllocationInputSchema, sandboxAllocationRequestSchema, selectableSandboxAllocationInputSchema, @@ -55,19 +54,23 @@ describe('sandbox allocation contract', () => { const request = getSandboxAllocationRequest(allocation); expect(sandboxAllocationInputSchema.parse(allocation)).toEqual(request); expect(sandboxAllocationInputSchema.parse(request)).toEqual(request); - expect(getKiloSandboxAllocation(request)).toBe(allocation); + expect(getSandboxAllocation(request)).toBe(allocation); } ); - it('keeps BYOC distinct from Kilo compute with the same instance type', () => { - const byoc = sandboxAllocationRequestSchema.parse({ - provider: { id: 'vercel', account: 'byoc' }, - instanceType: 'small', - }); - const kilo = getSandboxAllocationRequest('vercel-small'); - expect(getSandboxAllocationKey(byoc)).not.toBe(getSandboxAllocationKey(kilo)); - expect(getKiloSandboxAllocation(byoc)).toBeUndefined(); - expect(selectableSandboxAllocationInputSchema.parse(byoc)).toEqual(byoc); + it('accepts only BYOC Vercel and maps its selected size to the runtime allocation', () => { + const request = { provider: { id: 'vercel', account: 'byoc' }, instanceType: 'small' } as const; + expect(sandboxAllocationRequestSchema.parse(request)).toEqual( + getSandboxAllocationRequest('vercel-small') + ); + expect(getSandboxAllocation(request)).toBe('vercel-small'); + expect( + sandboxAllocationRequestSchema.safeParse({ + ...request, + provider: { id: 'vercel', account: 'kilo' }, + }).success + ).toBe(false); + expect(selectableSandboxAllocationInputSchema.parse(request)).toEqual(request); }); it.each([ @@ -106,7 +109,7 @@ describe('sandbox allocation contract', () => { it('accepts provider-default and devcontainer descriptions without making them selectable', () => { for (const defaultDestination of [ - { provider: { id: 'vercel', account: 'kilo' }, instanceType: 'default' }, + { provider: { id: 'vercel', account: 'byoc' }, instanceType: 'default' }, { provider: { id: 'cloudflare', account: 'kilo' }, instanceType: 'devcontainer' }, ]) { const capabilities = sandboxSelectionCapabilitiesSchema.parse({ diff --git a/packages/worker-utils/src/sandbox-allocation.ts b/packages/worker-utils/src/sandbox-allocation.ts index 3bbf4a8fe01..f5f2c0dc548 100644 --- a/packages/worker-utils/src/sandbox-allocation.ts +++ b/packages/worker-utils/src/sandbox-allocation.ts @@ -51,7 +51,7 @@ const cloudflareAllocationRequestSchema = z const vercelAllocationRequestSchema = z .object({ - provider: z.object({ id: z.literal('vercel'), account: z.enum(['kilo', 'byoc']) }).strict(), + provider: z.object({ id: z.literal('vercel'), account: z.literal('byoc') }).strict(), instanceType: z.enum(['small', 'large']), }) .strict(); @@ -113,11 +113,11 @@ const allocationRequests = { instanceType: 'standard-4', }, 'vercel-small': { - provider: { id: 'vercel', account: 'kilo' }, + provider: { id: 'vercel', account: 'byoc' }, instanceType: 'small', }, 'vercel-large': { - provider: { id: 'vercel', account: 'kilo' }, + provider: { id: 'vercel', account: 'byoc' }, instanceType: 'large', }, } as const satisfies Record; @@ -136,7 +136,7 @@ export function getSandboxAllocationKey( return `${allocation.provider.id}:${allocation.provider.account}:${allocation.instanceType}`; } -export function getKiloSandboxAllocation( +export function getSandboxAllocation( request: SandboxAllocationRequest ): SandboxAllocation | undefined { const key = getSandboxAllocationKey(request); @@ -261,7 +261,7 @@ export function isSandboxAllocationAvailable( allocation === 'cloudflare-single' || capabilities.options.some( option => - getKiloSandboxAllocation(option.allocation) === allocation && + getSandboxAllocation(option.allocation) === allocation && option.unavailableReason === undefined && option.repositoryUnavailableReason === undefined )) diff --git a/services/cloud-agent-next/.dev.vars.example b/services/cloud-agent-next/.dev.vars.example index 5d8efc10e8e..5059d40e0e7 100644 --- a/services/cloud-agent-next/.dev.vars.example +++ b/services/cloud-agent-next/.dev.vars.example @@ -70,23 +70,14 @@ CREDENTIAL_CONTAINMENT_ENABLED=false # binding and only applies to the constant isolated workspace path. CONTAINER_REPO_SNAPSHOT_IDS= -# Vercel Sandbox operational configuration (optional) -# Snapshot/build values must come from a successful `pnpm --filter cloud-agent-next snapshot:vercel` -# acceptance run. Partial configuration never enables Vercel. -VERCEL_PROJECT_ID= -VERCEL_TEAM_ID= -VERCEL_SANDBOX_SNAPSHOT_ID= -VERCEL_SANDBOX_RUNTIME_BUILD_ID= +# Vercel BYOC runtime defaults. Customer credentials and snapshots come from Compute settings. VERCEL_SANDBOX_RUNTIME=node24 VERCEL_SANDBOX_INITIAL_TIMEOUT_MS=300000 VERCEL_SANDBOX_EXTEND_DURATION_MS=600000 -# Keep `VERCEL_TOKEN` in local secrets/.dev.vars only; never commit a token value. -# VERCEL_TOKEN= -# Vercel enrollment is the allowlist itself. Empty is off. Comma-separated org -# IDs, or `*` for every organization and personal accounts. Organizations must -# also be isolated by PER_SESSION_SANDBOX_ORG_IDS. -VERCEL_SANDBOX_ORG_IDS= +# Customer-paid Vercel enrollment. Comma-separated user or organization IDs; empty is off. +# `*` enrolls both personal accounts and organizations. BYOC sessions use isolated sandboxes. +BYOC_VERCEL_IDS= # Cloudflare containers enrollment (optional). Comma-separated organization IDs, or `*` for # every organization and personal accounts. Empty is off. diff --git a/services/cloud-agent-next/docs/control-plane.md b/services/cloud-agent-next/docs/control-plane.md index 1f9c211af61..fd676bf3c9c 100644 --- a/services/cloud-agent-next/docs/control-plane.md +++ b/services/cloud-agent-next/docs/control-plane.md @@ -211,9 +211,10 @@ The mode says where the runtime (Kilo and the supervisor) comes from: the image and the SDK process scan stay unchanged. - **Selection.** `startup-selection.ts` reads the mode and the native image from Worker configuration once per create attempt. Absent configuration means `preinstalled`. Mode and image - are independent, but the runtime-free image requires `downloaded`. `downloaded` is native only: - SDK and Vercel pins on a downloaded target use `preinstalled`. Any other pair, a native - `downloaded` pin without a descriptor, or a missing descriptor or payload object fails + are independent, but the runtime-free image requires `downloaded`. Native and BYOC Vercel pins + support `downloaded`; SDK pins keep `preinstalled`. Vercel executes the trusted installer as root + against the customer snapshot. Its base digest excludes the downloaded CLI and wrapper release. + Any invalid pair, a `downloaded` pin without a descriptor, or a missing descriptor or payload object fails `invalid_configuration` before provider work. An R2 read error is a transient create error. - **State.** The repository-launch record stores the allocation's mode, and both record writers keep it. There is no other new @@ -844,8 +845,9 @@ On the native Cloudflare Containers runtime the container main process is the st after install. PID 1 identity (`/proc/1/cmdline`) is the issuance confirmation when it is either argv, bounded by the wrapper readiness deadline. PID 1 identity is not wrapper readiness: the wrapper socket and the Sandbox DO `starting` timer stay the readiness owner, and no provider poll or -wrapper-child probe is added for that path. Sandbox SDK and Vercel run the preinstalled startup -command against a living PID 1; downloaded startup is native-only. +wrapper-child probe is added for that path. Sandbox SDK runs the preinstalled startup command +against a living PID 1. Vercel runs either startup command against a living PID 1; downloaded startup +installs verified artifacts before it executes the supervisor. A native attempt that already issued the main process does not start another supervisor when that process is gone; the persisted `exec_pending` fence is not reset, and the next allocation may start a diff --git a/services/cloud-agent-next/docs/runtime-rollout.md b/services/cloud-agent-next/docs/runtime-rollout.md index 4f860cf0325..ad90d69ebb3 100644 --- a/services/cloud-agent-next/docs/runtime-rollout.md +++ b/services/cloud-agent-next/docs/runtime-rollout.md @@ -12,7 +12,15 @@ image from the catalog. Manual mode, descriptor or image drift fails before Wran | downloaded | runtime-free | Verified immutable base digest, exact-SHA runtime descriptor | The runtime-free/preinstalled pair is invalid in the shared configuration schema and the Worker. -Downloaded startup is native-only. SDK and Vercel keep preinstalled startup and their current adapters. +Downloaded startup supports native Cloudflare Containers and BYOC Vercel. SDK pins keep preinstalled startup. + +BYOC Vercel uses its customer-owned snapshot as the base. The snapshot setup provides the fixed Node +path and Bun version required by the bootstrap. Downloaded startup installs the selected immutable CLI +and control release as root. The base digest covers the ABI, Node major, Bun, and base setup commands; +CLI and wrapper changes do not change that digest or require a new snapshot. Existing allocations keep +their installed release until a new allocation starts. Base dependency changes still require a snapshot +upgrade. Upgrade older BYOC snapshots once before enabling downloaded mode, so they have the required +fixed Node path. Native image selection does not change the Vercel snapshot. ## Approval and rollout order diff --git a/services/cloud-agent-next/drizzle-vercel-snapshot.config.ts b/services/cloud-agent-next/drizzle-vercel-snapshot.config.ts new file mode 100644 index 00000000000..097eac56f8e --- /dev/null +++ b/services/cloud-agent-next/drizzle-vercel-snapshot.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + out: './drizzle/vercel-snapshot', + schema: './src/db/vercel-snapshot-schema.ts', + dialect: 'sqlite', + driver: 'durable-sqlite', +}); diff --git a/services/cloud-agent-next/drizzle/vercel-snapshot/0000_vercel_snapshot_build.sql b/services/cloud-agent-next/drizzle/vercel-snapshot/0000_vercel_snapshot_build.sql new file mode 100644 index 00000000000..537581fbbab --- /dev/null +++ b/services/cloud-agent-next/drizzle/vercel-snapshot/0000_vercel_snapshot_build.sql @@ -0,0 +1,31 @@ +CREATE TABLE `vercel_snapshot_builds` ( + `organization_id` text PRIMARY KEY NOT NULL, + `owner_type` text DEFAULT 'org' NOT NULL, + `credential_id` text NOT NULL, + `build_generation` text NOT NULL, + `runtime_build_id` text NOT NULL, + `step` text NOT NULL, + `retry_count` integer DEFAULT 0 NOT NULL, + `next_attempt_at` integer, + `builder_name` text NOT NULL, + `builder_operation_id` text NOT NULL, + `builder_create_requested` integer DEFAULT 0 NOT NULL, + `builder_session_id` text, + `validator_name` text NOT NULL, + `validator_operation_id` text NOT NULL, + `validator_create_requested` integer DEFAULT 0 NOT NULL, + `validator_session_id` text, + `validator_wrapper_requested` integer DEFAULT 0 NOT NULL, + `validator_wrapper_command_id` text, + `validator_control_id` text, + `team_slug` text, + `project_slug` text, + `snapshot_baseline_json` text, + `snapshot_source_session_id` text, + `snapshot_id` text, + `snapshot_candidate_id` text, + `snapshot_requested` integer DEFAULT 0 NOT NULL, + `last_error` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL +); diff --git a/services/cloud-agent-next/drizzle/vercel-snapshot/meta/0000_snapshot.json b/services/cloud-agent-next/drizzle/vercel-snapshot/meta/0000_snapshot.json new file mode 100644 index 00000000000..6ea8bf3e170 --- /dev/null +++ b/services/cloud-agent-next/drizzle/vercel-snapshot/meta/0000_snapshot.json @@ -0,0 +1,237 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "2077052a-8808-4849-92b0-a190291adb92", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "vercel_snapshot_builds": { + "name": "vercel_snapshot_builds", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "owner_type": { + "name": "owner_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'org'" + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "build_generation": { + "name": "build_generation", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_build_id": { + "name": "runtime_build_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step": { + "name": "step", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "builder_name": { + "name": "builder_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "builder_operation_id": { + "name": "builder_operation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "builder_create_requested": { + "name": "builder_create_requested", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "builder_session_id": { + "name": "builder_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "validator_name": { + "name": "validator_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validator_operation_id": { + "name": "validator_operation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validator_create_requested": { + "name": "validator_create_requested", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "validator_session_id": { + "name": "validator_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "validator_wrapper_requested": { + "name": "validator_wrapper_requested", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "validator_wrapper_command_id": { + "name": "validator_wrapper_command_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "validator_control_id": { + "name": "validator_control_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "team_slug": { + "name": "team_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "project_slug": { + "name": "project_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "snapshot_baseline_json": { + "name": "snapshot_baseline_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "snapshot_source_session_id": { + "name": "snapshot_source_session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "snapshot_id": { + "name": "snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "snapshot_candidate_id": { + "name": "snapshot_candidate_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "snapshot_requested": { + "name": "snapshot_requested", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/services/cloud-agent-next/drizzle/vercel-snapshot/meta/_journal.json b/services/cloud-agent-next/drizzle/vercel-snapshot/meta/_journal.json new file mode 100644 index 00000000000..07a3d47be05 --- /dev/null +++ b/services/cloud-agent-next/drizzle/vercel-snapshot/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1792875303360, + "tag": "0000_vercel_snapshot_build", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/services/cloud-agent-next/drizzle/vercel-snapshot/migrations.js b/services/cloud-agent-next/drizzle/vercel-snapshot/migrations.js new file mode 100644 index 00000000000..52481879dfe --- /dev/null +++ b/services/cloud-agent-next/drizzle/vercel-snapshot/migrations.js @@ -0,0 +1,9 @@ +import journal from './meta/_journal.json'; +import m0000 from './0000_vercel_snapshot_build.sql'; + +export default { + journal, + migrations: { + m0000, + }, +}; diff --git a/services/cloud-agent-next/package.json b/services/cloud-agent-next/package.json index 4bbf6c981b8..20da780bfa3 100644 --- a/services/cloud-agent-next/package.json +++ b/services/cloud-agent-next/package.json @@ -8,6 +8,7 @@ "type": "module", "scripts": { "preinstall": "npx only-allow pnpm", + "predeploy": "pnpm run build:wrapper", "deploy": "wrangler deploy", "predev": "pnpm run build:wrapper", "dev": "scripts/dev-with-docker-proxy.sh --no-proxy --env dev", diff --git a/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-runtime-config.test.ts b/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-runtime-config.test.ts index 2af9118c2a1..5828900bdd2 100644 --- a/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-runtime-config.test.ts +++ b/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-runtime-config.test.ts @@ -1,143 +1,22 @@ import { describe, expect, it } from 'vitest'; -import { - parseVercelSandboxCredentials, - parseVercelSandboxEnrollment, - parseVercelSandboxRuntimeConfig, - resolveVercelSandboxRuntimeConfig, -} from './vercel-runtime-config.js'; +import { parseVercelSandboxRuntimeDefaults } from './vercel-runtime-config.js'; -const completeRuntimeEnv = { - VERCEL_TOKEN: 'token', - VERCEL_TEAM_ID: 'team-id', - VERCEL_PROJECT_ID: 'project-id', - VERCEL_SANDBOX_SNAPSHOT_ID: 'snapshot-id', - VERCEL_SANDBOX_RUNTIME_BUILD_ID: 'build-id', - VERCEL_SANDBOX_RUNTIME: 'node24', - VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: '300000', - VERCEL_SANDBOX_EXTEND_DURATION_MS: '600000', -}; - -describe('parseVercelSandboxCredentials', () => { - it('parses the minimal exact-session cleanup configuration', () => { - expect( - parseVercelSandboxCredentials({ VERCEL_TOKEN: ' token ', VERCEL_TEAM_ID: ' team ' }) - ).toEqual({ accessToken: 'token', teamId: 'team' }); - expect(parseVercelSandboxCredentials({ VERCEL_TOKEN: 'token' })).toBeUndefined(); - }); -}); - -describe('parseVercelSandboxRuntimeConfig', () => { - it('parses complete operational configuration at the point of use', () => { - expect(parseVercelSandboxRuntimeConfig(completeRuntimeEnv)).toEqual({ - accessToken: 'token', - teamId: 'team-id', - projectId: 'project-id', - snapshotId: 'snapshot-id', - runtimeBuildId: 'build-id', +describe('BYOC Vercel runtime defaults', () => { + const env = { + VERCEL_SANDBOX_RUNTIME: 'node24', + VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: '300000', + VERCEL_SANDBOX_EXTEND_DURATION_MS: '600000', + }; + it('parses durations without platform credentials or snapshot configuration', () => { + expect(parseVercelSandboxRuntimeDefaults(env)).toEqual({ runtime: 'node24', initialTimeoutMs: 300000, extendDurationMs: 600000, }); }); - - it.each([ - 'VERCEL_TOKEN', - 'VERCEL_TEAM_ID', - 'VERCEL_PROJECT_ID', - 'VERCEL_SANDBOX_SNAPSHOT_ID', - 'VERCEL_SANDBOX_RUNTIME_BUILD_ID', - 'VERCEL_SANDBOX_RUNTIME', - 'VERCEL_SANDBOX_INITIAL_TIMEOUT_MS', - 'VERCEL_SANDBOX_EXTEND_DURATION_MS', - ] as const)('rejects configuration missing %s', key => { - const env = { ...completeRuntimeEnv }; - delete env[key]; - expect(parseVercelSandboxRuntimeConfig(env)).toBeUndefined(); - }); - - it.each([ - ['VERCEL_SANDBOX_RUNTIME', 'node22'], - ['VERCEL_SANDBOX_INITIAL_TIMEOUT_MS', '0'], - ['VERCEL_SANDBOX_INITIAL_TIMEOUT_MS', '1.5'], - ['VERCEL_SANDBOX_EXTEND_DURATION_MS', '-1'], - ['VERCEL_SANDBOX_EXTEND_DURATION_MS', 'not-a-number'], - ] as const)('rejects invalid %s', (key, value) => { + it.each(['0', '-1', 'invalid', '1.5'])('rejects invalid duration %s', duration => { expect( - parseVercelSandboxRuntimeConfig({ ...completeRuntimeEnv, [key]: value }) + parseVercelSandboxRuntimeDefaults({ ...env, VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: duration }) ).toBeUndefined(); }); }); - -describe('resolveVercelSandboxRuntimeConfig', () => { - const persisted = { - projectId: 'old-project', - snapshotId: 'old-snapshot', - runtimeBuildId: 'old-build', - runtime: 'node24', - }; - - it.each([ - { vcpus: 2, memory: 4096 }, - { vcpus: 4, memory: 8192 }, - ] as const)( - 'preserves persisted $vcpus vCPU resources across operational configuration changes', - resources => { - expect( - resolveVercelSandboxRuntimeConfig(completeRuntimeEnv, { ...persisted, resources }) - ).toMatchObject({ - ...persisted, - resources, - accessToken: completeRuntimeEnv.VERCEL_TOKEN, - }); - } - ); - - it('does not add sizing to older persisted runtime identities', () => { - expect(resolveVercelSandboxRuntimeConfig(completeRuntimeEnv, persisted)).not.toHaveProperty( - 'resources' - ); - expect(resolveVercelSandboxRuntimeConfig(completeRuntimeEnv)).not.toHaveProperty('resources'); - }); - - it.each([{ vcpus: 2, memory: 8192 }, { vcpus: 8, memory: 16384 }, null])( - 'rejects invalid persisted resources even when operational configuration is unavailable: %j', - resources => { - const input = { ...persisted, resources } as Parameters< - typeof resolveVercelSandboxRuntimeConfig - >[1]; - expect(() => resolveVercelSandboxRuntimeConfig(completeRuntimeEnv, input)).toThrow(); - expect(() => resolveVercelSandboxRuntimeConfig({}, input)).toThrow(); - } - ); -}); - -describe('parseVercelSandboxEnrollment', () => { - it('is disabled when the list is empty', () => { - expect(parseVercelSandboxEnrollment({})).toEqual({ - enabled: false, - orgIds: new Set(), - allowPersonal: false, - }); - expect(parseVercelSandboxEnrollment({ VERCEL_SANDBOX_ORG_IDS: ' , ' })).toEqual({ - enabled: false, - orgIds: new Set(), - allowPersonal: false, - }); - }); - - it('parses an explicit organization allowlist', () => { - expect( - parseVercelSandboxEnrollment({ - VERCEL_SANDBOX_ORG_IDS: ' org-a,org-b, org-a ', - }) - ).toEqual({ enabled: true, orgIds: new Set(['org-a', 'org-b']), allowPersonal: false }); - }); - - it('treats wildcard as every org plus personal', () => { - expect( - parseVercelSandboxEnrollment({ - VERCEL_SANDBOX_ORG_IDS: '*', - }) - ).toEqual({ enabled: true, orgIds: new Set(['*']), allowPersonal: true }); - }); -}); diff --git a/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-runtime-config.ts b/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-runtime-config.ts index edf659e342e..2e21db9779a 100644 --- a/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-runtime-config.ts +++ b/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-runtime-config.ts @@ -1,7 +1,4 @@ -import { - vercelSandboxResourcesSchema, - type VercelSandboxResources, -} from '@kilocode/worker-utils/sandbox-allocation'; +import type { VercelSandboxResources } from '@kilocode/worker-utils/sandbox-allocation'; import { z } from 'zod'; const positiveIntegerString = z @@ -10,20 +7,11 @@ const positiveIntegerString = z .transform(Number) .pipe(z.number().int().positive().safe()); -const runtimeConfigSchema = z.object({ - VERCEL_TOKEN: z.string().trim().min(1), - VERCEL_TEAM_ID: z.string().trim().min(1), - VERCEL_PROJECT_ID: z.string().trim().min(1), - VERCEL_SANDBOX_SNAPSHOT_ID: z.string().trim().min(1), - VERCEL_SANDBOX_RUNTIME_BUILD_ID: z.string().trim().min(1), - VERCEL_SANDBOX_RUNTIME: z.literal('node24'), - VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: positiveIntegerString, - VERCEL_SANDBOX_EXTEND_DURATION_MS: positiveIntegerString, -}); - export type VercelSandboxRuntimeConfig = { accessToken: string; teamId: string; + /** Project-scoped tokens must omit team-level query parameters. */ + scope?: 'team' | 'project'; projectId: string; snapshotId: string; runtimeBuildId: string; @@ -33,108 +21,39 @@ export type VercelSandboxRuntimeConfig = { extendDurationMs: number; }; +export type VercelSandboxRuntimeDefaults = Pick< + VercelSandboxRuntimeConfig, + 'runtime' | 'initialTimeoutMs' | 'extendDurationMs' +>; + export type VercelSandboxCredentials = { accessToken: string; teamId: string; + /** Project-scoped tokens must omit team-level query parameters. */ + scope?: 'team' | 'project'; }; export type VercelSandboxRuntimeConfigEnv = { - VERCEL_TOKEN?: string; - VERCEL_TEAM_ID?: string; - VERCEL_PROJECT_ID?: string; - VERCEL_SANDBOX_SNAPSHOT_ID?: string; - VERCEL_SANDBOX_RUNTIME_BUILD_ID?: string; VERCEL_SANDBOX_RUNTIME?: string; VERCEL_SANDBOX_INITIAL_TIMEOUT_MS?: string; VERCEL_SANDBOX_EXTEND_DURATION_MS?: string; }; -export function parseVercelSandboxCredentials( - env: VercelSandboxRuntimeConfigEnv -): VercelSandboxCredentials | undefined { - const accessToken = env.VERCEL_TOKEN?.trim(); - const teamId = env.VERCEL_TEAM_ID?.trim(); - return accessToken && teamId ? { accessToken, teamId } : undefined; -} - -export function parseVercelSandboxRuntimeConfig( +export function parseVercelSandboxRuntimeDefaults( env: VercelSandboxRuntimeConfigEnv -): VercelSandboxRuntimeConfig | undefined { - const result = runtimeConfigSchema.safeParse(env); +): VercelSandboxRuntimeDefaults | undefined { + const result = z + .object({ + VERCEL_SANDBOX_RUNTIME: z.literal('node24'), + VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: positiveIntegerString, + VERCEL_SANDBOX_EXTEND_DURATION_MS: positiveIntegerString, + }) + .safeParse(env); if (!result.success) return undefined; return { - accessToken: result.data.VERCEL_TOKEN, - teamId: result.data.VERCEL_TEAM_ID, - projectId: result.data.VERCEL_PROJECT_ID, - snapshotId: result.data.VERCEL_SANDBOX_SNAPSHOT_ID, - runtimeBuildId: result.data.VERCEL_SANDBOX_RUNTIME_BUILD_ID, runtime: result.data.VERCEL_SANDBOX_RUNTIME, initialTimeoutMs: result.data.VERCEL_SANDBOX_INITIAL_TIMEOUT_MS, extendDurationMs: result.data.VERCEL_SANDBOX_EXTEND_DURATION_MS, }; } - -/** - * Operational config for a session's Vercel runtime: environment values, - * overlaid with the session's persisted runtime identity once one exists so - * later env rotations cannot repoint an already-created runtime. - */ -export function resolveVercelSandboxRuntimeConfig( - env: VercelSandboxRuntimeConfigEnv, - persisted?: { - projectId?: string; - snapshotId?: string; - runtimeBuildId?: string; - runtime?: string; - resources?: VercelSandboxResources; - } -): VercelSandboxRuntimeConfig | undefined { - const resources = vercelSandboxResourcesSchema.optional().parse(persisted?.resources); - const configured = parseVercelSandboxRuntimeConfig(env); - if (!configured) return undefined; - const resolved: VercelSandboxRuntimeConfig = - persisted?.projectId && - persisted.snapshotId && - persisted.runtimeBuildId && - persisted.runtime === 'node24' - ? { - ...configured, - projectId: persisted.projectId, - snapshotId: persisted.snapshotId, - runtimeBuildId: persisted.runtimeBuildId, - runtime: persisted.runtime, - } - : configured; - return resources === undefined ? resolved : { ...resolved, resources }; -} - -export type VercelSandboxEnrollmentEnv = { - VERCEL_SANDBOX_ORG_IDS?: string; -}; - -export type VercelSandboxEnrollment = { - enabled: boolean; - orgIds: Set; - allowPersonal: boolean; -}; - -export function parseVercelSandboxEnrollment( - env: VercelSandboxEnrollmentEnv -): VercelSandboxEnrollment { - const orgIds = new Set( - (env.VERCEL_SANDBOX_ORG_IDS ?? '') - .split(',') - .map(orgId => orgId.trim()) - .filter(Boolean) - ); - if (orgIds.size === 0) { - return { enabled: false, orgIds, allowPersonal: false }; - } - - return { - enabled: true, - orgIds, - allowPersonal: orgIds.has('*'), - }; -} diff --git a/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-sandbox-rest-client.test.ts b/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-sandbox-rest-client.test.ts index 56145b37dc1..aba9d11d7af 100644 --- a/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-sandbox-rest-client.test.ts +++ b/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-sandbox-rest-client.test.ts @@ -9,6 +9,7 @@ import { type VercelSandboxNetworkPolicy, type VercelSandboxResource, type VercelSandboxSession, + type VercelSandboxSnapshot, } from './vercel-sandbox-rest-client.js'; const sandboxName = 'ses-exact-runtime'; @@ -66,11 +67,21 @@ function command(overrides: Partial = {}): VercelSandboxCo }; } -function clientFor(providerFetch: typeof fetch) { +function snapshot(overrides: Partial = {}): VercelSandboxSnapshot { + return { + id: 'snap_base', + sourceSessionId: sessionId, + status: 'created', + ...overrides, + }; +} + +function clientFor(providerFetch: typeof fetch, scope?: 'team' | 'project') { return new VercelSandboxRestClient({ accessToken: 'secret-access-token', projectId: 'prj_test', teamId: 'team_test', + scope, fetch: providerFetch, }); } @@ -112,6 +123,17 @@ function networkPolicy(): VercelSandboxNetworkPolicy { }; } +function runtimeCreateInput() { + return { + name: sandboxName, + operationId: 'operation-123', + runtimeBuildId: 'runtime-build-123', + source: { type: 'runtime' as const }, + runtime: 'node24' as const, + timeoutMs: 300_000, + }; +} + function jsonResponse(body: unknown, init?: ResponseInit): Response { return Response.json(body, init); } @@ -245,6 +267,35 @@ describe('VercelSandboxRestClient', () => { } }); + it('omits team scope parameters for a project-scoped token', async () => { + const providerFetch = vi.fn().mockResolvedValue( + jsonResponse({ + sandbox: sandbox(), + session: session(), + routes: [], + }) + ); + + await clientFor(providerFetch, 'project').createSandbox(createInput()); + + expect(providerFetch.mock.calls[0]?.[0]).toBe('https://api.vercel.com/v2/sandboxes'); + }); + + it('validates the inferred project account without a team scope parameter', async () => { + const providerFetch = vi + .fn() + .mockResolvedValue( + jsonResponse({ id: 'prj_test', name: 'test-project', accountId: 'team_test' }) + ); + + await expect(clientFor(providerFetch, 'project').inspectProject()).resolves.toEqual({ + id: 'prj_test', + name: 'test-project', + accountId: 'team_test', + }); + expect(providerFetch.mock.calls[0]?.[0]).toBe('https://api.vercel.com/v9/projects/prj_test'); + }); + it('creates a contained sandbox with a nested REST-native policy and redirects disabled', async () => { const providerFetch = vi .fn() @@ -272,6 +323,103 @@ describe('VercelSandboxRestClient', () => { }); }); + it('omits source entirely when creating a sandbox directly from its runtime', async () => { + const providerFetch = vi.fn().mockResolvedValue( + jsonResponse({ + sandbox: sandbox(), + session: session({ sourceSnapshotId: undefined }), + routes: [], + }) + ); + + const result = await clientFor(providerFetch).createSandbox(runtimeCreateInput()); + + expect(result.runtime).toEqual({ sandboxName, sessionId }); + const body = JSON.parse(providerFetch.mock.calls[0][1].body as string); + expect(body).toEqual({ + projectId: 'prj_test', + name: sandboxName, + runtime: 'node24', + timeout: 300_000, + persistent: false, + tags: { + [VERCEL_CLOUD_AGENT_RESOURCE_TAG]: VERCEL_CLOUD_AGENT_RESOURCE_TAG_VALUE, + [VERCEL_CLOUD_AGENT_CREATE_OPERATION_TAG]: 'operation-123', + [VERCEL_CLOUD_AGENT_RUNTIME_BUILD_TAG]: 'runtime-build-123', + }, + }); + expect(body).not.toHaveProperty('source'); + }); + + it('preserves an explicitly discriminated snapshot source in sandbox requests', async () => { + const providerFetch = vi + .fn() + .mockResolvedValue(jsonResponse({ sandbox: sandbox(), session: session(), routes: [] })); + const { snapshotId, ...input } = createInput(); + + await clientFor(providerFetch).createSandbox({ + ...input, + source: { type: 'snapshot', snapshotId }, + }); + + expect(JSON.parse(providerFetch.mock.calls[0][1].body as string)).toMatchObject({ + source: { type: 'snapshot', snapshotId: 'snap_base' }, + }); + }); + + it('rejects a runtime-created sandbox response that contains a snapshot source', async () => { + const client = clientFor( + vi + .fn() + .mockResolvedValue(jsonResponse({ sandbox: sandbox(), session: session(), routes: [] })) + ); + + await expect(client.createSandbox(runtimeCreateInput())).rejects.toMatchObject({ + kind: 'correlation_mismatch', + operation: 'create', + }); + }); + + it.each(['create', 'inspect'] as const)( + 'rejects invalid legacy and explicit snapshot identifiers before %s', + async operation => { + const providerFetch = vi.fn(); + const client = clientFor(providerFetch); + for (const snapshotId of ['', 's'.repeat(257)]) { + for (const request of [ + { ...createInput(), snapshotId }, + { ...runtimeCreateInput(), source: { type: 'snapshot' as const, snapshotId } }, + ]) { + await expect( + operation === 'create' ? client.createSandbox(request) : client.inspectByName(request) + ).rejects.toMatchObject({ kind: 'invalid_request', operation }); + } + } + expect(providerFetch).not.toHaveBeenCalled(); + } + ); + + it('keeps the creation network policy when using an explicit runtime source', async () => { + const providerFetch = vi.fn().mockResolvedValue( + jsonResponse({ + sandbox: sandbox(), + session: session({ sourceSnapshotId: undefined }), + routes: [], + }) + ); + const policy = networkPolicy(); + + await clientFor(providerFetch).createSandbox({ + ...runtimeCreateInput(), + networkPolicy: policy, + }); + + const init = providerFetch.mock.calls[0][1]; + expect(init.redirect).toBe('manual'); + expect(JSON.parse(init.body as string)).toMatchObject({ networkPolicy: policy }); + expect(JSON.parse(init.body as string)).not.toHaveProperty('source'); + }); + it('invokes the injected fetch as a free function', async () => { function providerFetch(this: unknown, _input: RequestInfo | URL, _init?: RequestInit) { if (this !== undefined && this !== globalThis) { @@ -339,6 +487,142 @@ describe('VercelSandboxRestClient', () => { await expect(client.inspectByName(createInput())).resolves.toBeNull(); }); + it('collects every snapshot page with the same project and team scope', async () => { + const first = snapshot({ id: 'snap-first' }); + const second = snapshot({ id: 'snap-second' }); + const providerFetch = vi + .fn() + .mockResolvedValueOnce( + jsonResponse({ snapshots: [first], pagination: { count: 1, next: 'next/page' } }) + ) + .mockResolvedValueOnce( + jsonResponse({ snapshots: [second], pagination: { count: 1, next: null } }) + ); + + await expect(clientFor(providerFetch).listSnapshots()).resolves.toEqual([first, second]); + expect(providerFetch.mock.calls.map(([url]) => url)).toEqual([ + 'https://api.vercel.com/v2/sandboxes/snapshots?teamId=team_test&project=prj_test&limit=50', + 'https://api.vercel.com/v2/sandboxes/snapshots?teamId=team_test&project=prj_test&limit=50&cursor=next%2Fpage', + ]); + }); + + it('rejects a repeated snapshot cursor instead of returning an incomplete list', async () => { + const providerFetch = vi.fn().mockImplementation(async () => + jsonResponse({ + snapshots: [snapshot()], + pagination: { count: 1, next: 'repeated' }, + }) + ); + await expect(clientFor(providerFetch).listSnapshots()).rejects.toThrow('invalid_response'); + expect(providerFetch).toHaveBeenCalledTimes(2); + }); + + it('propagates a later snapshot page failure instead of returning partial results', async () => { + const providerFetch = vi + .fn() + .mockResolvedValueOnce( + jsonResponse({ snapshots: [snapshot()], pagination: { count: 1, next: 'next' } }) + ) + .mockResolvedValueOnce(new Response(null, { status: 503 })); + await expect(clientFor(providerFetch).listSnapshots()).rejects.toThrow('request_failed'); + }); + + it('defaults newly created snapshots to a finite 30-day retention period', async () => { + const providerFetch = vi.fn().mockResolvedValue(jsonResponse({ snapshot: snapshot() })); + + await expect(clientFor(providerFetch).createSnapshot(sessionId)).resolves.toEqual(snapshot()); + + const [url, init] = providerFetch.mock.calls[0]; + expect(url).toBe( + 'https://api.vercel.com/v2/sandboxes/sessions/sbox_session_exact/snapshot?teamId=team_test' + ); + expect(JSON.parse(init.body as string)).toEqual({ expiration: 30 * 24 * 60 * 60 * 1000 }); + }); + + it('preserves explicitly configured finite snapshot retention', async () => { + const providerFetch = vi.fn().mockResolvedValue(jsonResponse({ snapshot: snapshot() })); + const expiration = 7 * 24 * 60 * 60 * 1000; + + await clientFor(providerFetch).createSnapshot(sessionId, expiration); + + expect(JSON.parse(providerFetch.mock.calls[0][1].body as string)).toEqual({ expiration }); + }); + + it('deletes the exact snapshot with authenticated team-scoped provider access', async () => { + const timeout = vi.spyOn(AbortSignal, 'timeout'); + const providerFetch = vi + .fn() + .mockResolvedValue(jsonResponse({ snapshot: snapshot({ status: 'deleted' }) })); + + await expect(clientFor(providerFetch).deleteSnapshot('snap_base')).resolves.toBeUndefined(); + + const [url, init] = providerFetch.mock.calls[0]; + expect(url).toBe('https://api.vercel.com/v2/sandboxes/snapshots/snap_base?teamId=team_test'); + expect(init.method).toBe('DELETE'); + expect(new Headers(init.headers).get('authorization')).toBe('Bearer secret-access-token'); + expect(init.signal).toBeInstanceOf(AbortSignal); + expect(timeout).toHaveBeenCalledWith(30_000); + timeout.mockRestore(); + }); + + it.each([404, 410])('treats snapshot deletion status %s as already deleted', async status => { + const providerFetch = vi.fn().mockResolvedValue(new Response('already deleted', { status })); + + await expect(clientFor(providerFetch).deleteSnapshot('snap_base')).resolves.toBeUndefined(); + }); + + it.each([ + ['snapshot ID', snapshot({ id: 'snap_other', status: 'deleted' })], + ['snapshot status', snapshot({ status: 'created' })], + ])('rejects a deleted snapshot response with mismatched %s', async (_field, deletedSnapshot) => { + const client = clientFor( + vi.fn().mockResolvedValue(jsonResponse({ snapshot: deletedSnapshot })) + ); + + await expect(client.deleteSnapshot('snap_base')).rejects.toMatchObject({ + kind: 'correlation_mismatch', + operation: 'delete-snapshot', + }); + }); + + it('rejects an invalid snapshot deletion response envelope', async () => { + const client = clientFor( + vi.fn().mockResolvedValue(jsonResponse({ snapshot: { id: 'snap_base' } })) + ); + + await expect(client.deleteSnapshot('snap_base')).rejects.toMatchObject({ + kind: 'invalid_response', + operation: 'delete-snapshot', + }); + }); + + it('classifies snapshot deletion failures without exposing provider response contents', async () => { + const reflectedSecret = 'secret-access-token reflected-provider-body'; + const client = clientFor( + vi.fn().mockResolvedValue(new Response(reflectedSecret, { status: 403 })) + ); + + const error = await client.deleteSnapshot('snap_base').catch(cause => cause); + + expect(error).toMatchObject({ + kind: 'request_failed', + operation: 'delete-snapshot', + status: 403, + message: 'Vercel Sandbox delete-snapshot failed (request_failed, status 403)', + }); + expect(String(error)).not.toContain(reflectedSecret); + }); + + it('rejects an invalid snapshot deletion target before calling the provider', async () => { + const providerFetch = vi.fn(); + + await expect(clientFor(providerFetch).deleteSnapshot('')).rejects.toMatchObject({ + kind: 'invalid_request', + operation: 'delete-snapshot', + }); + expect(providerFetch).not.toHaveBeenCalled(); + }); + it('gets and correlates an exact session', async () => { const providerFetch = vi .fn() diff --git a/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-sandbox-rest-client.ts b/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-sandbox-rest-client.ts index 34eb6a4fce2..cddfe672d25 100644 --- a/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-sandbox-rest-client.ts +++ b/services/cloud-agent-next/src/agent-sandbox/vercel/vercel-sandbox-rest-client.ts @@ -15,6 +15,7 @@ const OBSERVATION_CONTROL_DEADLINE_MS = 30_000; const STANDARD_REQUEST_DEADLINE_MS = 120_000; const WAITED_COMMAND_TRANSPORT_ALLOWANCE_MS = 10_000; const MAX_PROVIDER_REQUEST_DEADLINE_MS = 300_000; +const DEFAULT_SNAPSHOT_EXPIRATION_MS = 30 * 24 * 60 * 60 * 1000; export const VERCEL_CLOUD_AGENT_RESOURCE_TAG = 'kilo-managed-by'; export const VERCEL_CLOUD_AGENT_RESOURCE_TAG_VALUE = 'cloud-agent-session'; @@ -89,12 +90,33 @@ const sessionAndRoutesEnvelopeSchema = sessionEnvelopeSchema.extend({ const commandEnvelopeSchema = z.object({ command: commandSchema }); const finishedCommandEnvelopeSchema = z.object({ command: finishedCommandSchema }); const commandsEnvelopeSchema = z.object({ commands: z.array(commandSchema) }); +const snapshotSchema = z + .object({ + id: z.string().min(1), + sourceSessionId: z.string().min(1), + status: z.string().min(1), + createdAt: z.number().optional(), + expiresAt: z.number().optional(), + }) + .passthrough(); +const snapshotEnvelopeSchema = z.object({ snapshot: snapshotSchema }); +const snapshotsEnvelopeSchema = z.object({ + snapshots: z.array(snapshotSchema), + pagination: z.object({ next: z.string().min(1).nullable() }), +}); +const teamDetailsSchema = z + .object({ id: z.string().min(1), slug: z.string().min(1) }) + .passthrough(); +const projectDetailsSchema = z + .object({ id: z.string().min(1), name: z.string().min(1), accountId: z.string().min(1) }) + .passthrough(); export type VercelSandboxRuntime = z.infer; export type VercelSandboxResource = z.infer; export type VercelSandboxSession = z.infer; export type VercelSandboxRoute = z.infer; export type VercelSandboxCommand = z.infer; +export type VercelSandboxSnapshot = z.infer; export type VercelSandboxMatcher = { exact: string } | { startsWith: string }; @@ -118,20 +140,45 @@ export type VercelSandboxRestClientConfig = { accessToken: string; projectId?: string; teamId: string; + scope?: 'team' | 'project'; fetch: typeof fetch; }; -export type CreateSandboxInput = { +type CreateSandboxInputBase = { name: string; operationId: string; runtimeBuildId: string; - snapshotId: string; runtime: VercelSandboxRuntime; timeoutMs: number; resources?: VercelSandboxResources; networkPolicy?: VercelSandboxNetworkPolicy; }; +/** Source is explicit so a builder cannot accidentally boot from a runtime snapshot. */ +export type CreateSandboxInput = + | (CreateSandboxInputBase & { + source: { type: 'runtime' }; + /** Compatibility-only input for older callers; ignored when source is present. */ + snapshotId?: never; + }) + | (CreateSandboxInputBase & { + source: { type: 'snapshot'; snapshotId: string }; + /** Compatibility-only input for older callers; ignored when source is present. */ + snapshotId?: never; + }) + | (CreateSandboxInputBase & { + /** Legacy snapshot form retained for existing platform sessions. */ + snapshotId: string; + source?: never; + }); + +export type CreateSandboxInspectInput = Omit & + ( + | { source: { type: 'runtime' }; snapshotId?: never } + | { source: { type: 'snapshot'; snapshotId: string }; snapshotId?: never } + | { snapshotId: string; source?: never } + ); + export type VercelSandboxCreateEnvelope = { sandbox: VercelSandboxResource; session: VercelSandboxSession; @@ -171,7 +218,13 @@ type VercelSandboxOperation = | 'read-file' | 'extend-timeout' | 'update-network-policy' - | 'stop-session'; + | 'stop-session' + | 'create-snapshot' + | 'delete-snapshot' + | 'list-snapshots' + | 'get-snapshot' + | 'get-team' + | 'get-project'; export type VercelSandboxErrorKind = | 'invalid_configuration' | 'invalid_request' @@ -296,7 +349,6 @@ export class VercelSandboxRestClient { requireSandboxName(input.name, operation); requireIdentifier(input.operationId, operation); requireIdentifier(input.runtimeBuildId, operation); - requireIdentifier(input.snapshotId, operation); requirePositiveDuration(input.timeoutMs, operation); if ( !runtimeSchema.safeParse(input.runtime).success || @@ -304,6 +356,7 @@ export class VercelSandboxRestClient { ) { throw new VercelSandboxRestError('invalid_request', operation); } + const source = sourceForInput(input, operation); const response = await this.fetchProvider(operation, this.collectionUrl(), { method: 'POST', @@ -311,7 +364,7 @@ export class VercelSandboxRestClient { body: JSON.stringify({ projectId, name: input.name, - source: { type: 'snapshot', snapshotId: input.snapshotId }, + ...(source.type === 'snapshot' ? { source } : {}), runtime: input.runtime, timeout: input.timeoutMs, persistent: false, @@ -331,20 +384,20 @@ export class VercelSandboxRestClient { } async inspectByName( - input: Omit + input: CreateSandboxInspectInput ): Promise { const operation = 'inspect'; const projectId = this.requireProjectConfiguration(operation); requireSandboxName(input.name, operation); requireIdentifier(input.operationId, operation); requireIdentifier(input.runtimeBuildId, operation); - requireIdentifier(input.snapshotId, operation); if ( !runtimeSchema.safeParse(input.runtime).success || !vercelSandboxResourcesSchema.optional().safeParse(input.resources).success ) { throw new VercelSandboxRestError('invalid_request', operation); } + sourceForInput(input, operation); const url = this.namedSandboxUrl(input.name, projectId); url.searchParams.set('resume', 'false'); const response = await this.fetchProvider(operation, url, { method: 'GET' }); @@ -355,6 +408,130 @@ export class VercelSandboxRestClient { return this.withRuntime(envelope); } + async createSnapshot( + sessionId: string, + expiration = DEFAULT_SNAPSHOT_EXPIRATION_MS + ): Promise { + const operation = 'create-snapshot'; + this.validateConfiguration(operation); + requireIdentifier(sessionId, operation); + this.requireProjectConfiguration(operation); + if (!Number.isInteger(expiration) || expiration < 0) { + throw new VercelSandboxRestError('invalid_request', operation); + } + const response = await this.fetchProvider( + operation, + this.sessionActionUrl(sessionId, 'snapshot'), + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ expiration }), + }, + STANDARD_REQUEST_DEADLINE_MS + ); + await this.requireSuccessfulResponse(response, operation); + const envelope = await this.parseJson(response, snapshotEnvelopeSchema, operation); + if (envelope.snapshot.sourceSessionId !== sessionId || envelope.snapshot.status !== 'created') { + throw new VercelSandboxRestError('correlation_mismatch', operation); + } + return envelope.snapshot; + } + + async deleteSnapshot(snapshotId: string): Promise { + const operation = 'delete-snapshot'; + this.validateConfiguration(operation); + requireIdentifier(snapshotId, operation); + const url = new URL( + `/v2/sandboxes/snapshots/${encodeURIComponent(snapshotId)}`, + VERCEL_SANDBOX_API_BASE_URL + ); + this.applyTeamScope(url); + const response = await this.fetchProvider(operation, url, { method: 'DELETE' }); + if (response.status === 404 || response.status === 410) { + await response.body?.cancel().catch(() => undefined); + return; + } + await this.requireSuccessfulResponse(response, operation); + const snapshot = (await this.parseJson(response, snapshotEnvelopeSchema, operation)).snapshot; + if (snapshot.id !== snapshotId || snapshot.status !== 'deleted') { + throw new VercelSandboxRestError('correlation_mismatch', operation); + } + } + + async listSnapshots(): Promise { + const operation = 'list-snapshots'; + const projectId = this.requireProjectConfiguration(operation); + const url = new URL('/v2/sandboxes/snapshots', VERCEL_SANDBOX_API_BASE_URL); + this.applyTeamScope(url); + url.searchParams.set('project', projectId); + url.searchParams.set('limit', '50'); + const snapshots: VercelSandboxSnapshot[] = []; + const cursors = new Set(); + for (let page = 0; page < 100; page++) { + const response = await this.fetchProvider(operation, url, { method: 'GET' }); + await this.requireSuccessfulResponse(response, operation); + const result = await this.parseJson(response, snapshotsEnvelopeSchema, operation); + snapshots.push(...result.snapshots); + const cursor = result.pagination.next; + if (cursor === null) return snapshots; + if (cursors.has(cursor)) throw new VercelSandboxRestError('invalid_response', operation); + cursors.add(cursor); + url.searchParams.set('cursor', cursor); + } + throw new VercelSandboxRestError('invalid_response', operation); + } + + async inspectSnapshot(snapshotId: string): Promise { + const operation = 'get-snapshot'; + this.validateConfiguration(operation); + requireIdentifier(snapshotId, operation); + const url = new URL( + `/v2/sandboxes/snapshots/${encodeURIComponent(snapshotId)}`, + VERCEL_SANDBOX_API_BASE_URL + ); + this.applyTeamScope(url); + const response = await this.fetchProvider(operation, url, { method: 'GET' }); + await this.requireSuccessfulResponse(response, operation); + const snapshot = (await this.parseJson(response, snapshotEnvelopeSchema, operation)).snapshot; + if (snapshot.id !== snapshotId || snapshot.status !== 'created') { + throw new VercelSandboxRestError('correlation_mismatch', operation); + } + return snapshot; + } + + async inspectTeam(): Promise<{ id: string; slug: string }> { + const operation = 'get-team'; + this.validateConfiguration(operation); + const response = await this.fetchProvider( + operation, + new URL(`/v2/teams/${encodeURIComponent(this.config.teamId)}`, VERCEL_SANDBOX_API_BASE_URL), + { method: 'GET' } + ); + await this.requireSuccessfulResponse(response, operation); + const team = await this.parseJson(response, teamDetailsSchema, operation); + if (team.id !== this.config.teamId) { + throw new VercelSandboxRestError('correlation_mismatch', operation); + } + return team; + } + + async inspectProject(): Promise<{ id: string; name: string; accountId: string }> { + const operation = 'get-project'; + const projectId = this.requireProjectConfiguration(operation); + const url = new URL( + `/v9/projects/${encodeURIComponent(projectId)}`, + VERCEL_SANDBOX_API_BASE_URL + ); + this.applyTeamScope(url); + const response = await this.fetchProvider(operation, url, { method: 'GET' }); + await this.requireSuccessfulResponse(response, operation); + const project = await this.parseJson(response, projectDetailsSchema, operation); + if (project.id !== projectId || project.accountId !== this.config.teamId) { + throw new VercelSandboxRestError('correlation_mismatch', operation); + } + return project; + } + async getSession( sessionId: string, expectedSandboxName: string @@ -453,7 +630,7 @@ export class VercelSandboxRestClient { `${this.commandUrl(sessionId, commandId).pathname}/kill`, VERCEL_SANDBOX_API_BASE_URL ); - url.searchParams.set('teamId', this.config.teamId); + this.applyTeamScope(url); const response = await this.fetchProvider(operation, url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -558,7 +735,7 @@ export class VercelSandboxRestClient { private correlateCreateEnvelope( envelope: z.infer, - input: Omit, + input: CreateSandboxInspectInput, operation: VercelSandboxOperation ): void { const tags = envelope.sandbox.tags; @@ -568,7 +745,7 @@ export class VercelSandboxRestClient { envelope.sandbox.currentSessionId !== envelope.session.id || envelope.session.sourceSandboxName !== input.name || envelope.session.projectId !== this.config.projectId || - envelope.session.sourceSnapshotId !== input.snapshotId || + envelope.session.sourceSnapshotId !== sourceSnapshotId(input, operation) || envelope.session.runtime !== input.runtime || (input.resources !== undefined && (envelope.session.vcpus !== input.resources.vcpus || @@ -624,14 +801,14 @@ export class VercelSandboxRestClient { private collectionUrl(): URL { const url = new URL('/v2/sandboxes', VERCEL_SANDBOX_API_BASE_URL); - url.searchParams.set('teamId', this.config.teamId); + this.applyTeamScope(url); return url; } private namedSandboxUrl(name: string, projectId: string): URL { const url = new URL(`/v2/sandboxes/${encodeURIComponent(name)}`, VERCEL_SANDBOX_API_BASE_URL); url.searchParams.set('projectId', projectId); - url.searchParams.set('teamId', this.config.teamId); + this.applyTeamScope(url); return url; } @@ -640,13 +817,13 @@ export class VercelSandboxRestClient { `/v2/sandboxes/sessions/${encodeURIComponent(sessionId)}`, VERCEL_SANDBOX_API_BASE_URL ); - url.searchParams.set('teamId', this.config.teamId); + this.applyTeamScope(url); return url; } private commandsUrl(sessionId: string): URL { const url = new URL(`${this.sessionUrl(sessionId).pathname}/cmd`, VERCEL_SANDBOX_API_BASE_URL); - url.searchParams.set('teamId', this.config.teamId); + this.applyTeamScope(url); return url; } @@ -655,7 +832,7 @@ export class VercelSandboxRestClient { `${this.commandsUrl(sessionId).pathname}/${encodeURIComponent(commandId)}`, VERCEL_SANDBOX_API_BASE_URL ); - url.searchParams.set('teamId', this.config.teamId); + this.applyTeamScope(url); return url; } @@ -664,7 +841,7 @@ export class VercelSandboxRestClient { `${this.sessionUrl(sessionId).pathname}/${action}`, VERCEL_SANDBOX_API_BASE_URL ); - url.searchParams.set('teamId', this.config.teamId); + this.applyTeamScope(url); return url; } @@ -672,6 +849,10 @@ export class VercelSandboxRestClient { return this.sessionActionUrl(sessionId, `fs/${action}`); } + private applyTeamScope(url: URL): void { + if (this.config.scope !== 'project') url.searchParams.set('teamId', this.config.teamId); + } + private requestDeadlineMs(operation: VercelSandboxOperation): number { switch (operation) { case 'create': @@ -830,3 +1011,25 @@ export class VercelSandboxRestClient { } } } + +type CreateSandboxSourceInput = { + source?: { type: 'runtime' } | { type: 'snapshot'; snapshotId: string }; + snapshotId?: string; +}; + +function sourceForInput( + input: CreateSandboxSourceInput, + operation: VercelSandboxOperation +): { type: 'runtime' } | { type: 'snapshot'; snapshotId: string } { + const source = input.source ?? { type: 'snapshot', snapshotId: input.snapshotId ?? '' }; + if (source.type === 'snapshot') requireIdentifier(source.snapshotId, operation); + return source; +} + +function sourceSnapshotId( + input: CreateSandboxSourceInput, + operation: VercelSandboxOperation +): string | undefined { + const source = sourceForInput(input, operation); + return source.type === 'snapshot' ? source.snapshotId : undefined; +} diff --git a/services/cloud-agent-next/src/byoc/vercel-credential-resolver.test.ts b/services/cloud-agent-next/src/byoc/vercel-credential-resolver.test.ts new file mode 100644 index 00000000000..a822778b557 --- /dev/null +++ b/services/cloud-agent-next/src/byoc/vercel-credential-resolver.test.ts @@ -0,0 +1,424 @@ +import { generateKeyPairSync } from 'node:crypto'; +import type { VercelComputeCredentialEnvelope } from '@kilocode/db/schema-types'; +import { + decryptSecrets, + encryptKeyedEnvelope, + encryptWithPublicKey, + parseKeyedEnvelope, +} from '@kilocode/encryption'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { Env } from '../types.js'; +import { + ByocCredentialResolverError, + fetchByocVercelCredential, + fetchByocVercelEnrollmentForUser, + projectByocVercelStatus, + projectByocVercelSnapshotMissing, + resolveByocVercelCredentials, + resolveByocVercelRuntimeConfig, +} from './vercel-credential-resolver.js'; + +const keys = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, +}); + +const scheme = + 'byoc-vercel-credential-rsa-aes-256-gcm' satisfies VercelComputeCredentialEnvelope['scheme']; +const keyId = 'agent-env-vars-v1'; +const accessToken = 'vercel-secret-token'; +const identity = { + organizationId: '11111111-1111-4111-8111-111111111111', + credentialId: '22222222-2222-4222-8222-222222222222', +}; +const personalIdentity = { + userId: 'oauth/vercel-user', + credentialId: '44444444-4444-4444-8444-444444444444', +}; +const snapshot = { + ...identity, + buildGeneration: '33333333-3333-4333-8333-333333333333', + runtimeSnapshotId: 'snapshot-1', +}; + +const env = { + KILOCODE_BACKEND_BASE_URL: 'https://backend.example.test', + AGENT_ENV_VARS_PRIVATE_KEY: keys.privateKey, + INTERNAL_API_SECRET_PROD: { get: async () => 'internal-secret' }, +} as Env; + +type CredentialOwnerInput = { + credentialId: string; + tokenEncrypted?: unknown; +} & ({ organizationId: string } | { userId: string }); + +function encryptCredentialToken( + input: CredentialOwnerInput = identity +): VercelComputeCredentialEnvelope { + const ownerKey = 'organizationId' in input ? input.organizationId : `user:${input.userId}`; + return parseKeyedEnvelope( + encryptKeyedEnvelope( + accessToken, + scheme, + { keyId, publicKeyPem: keys.publicKey }, + `byoc-vercel-credential:v1:${ownerKey}:${input.credentialId}` + ), + scheme + ); +} + +function credentialResponse(input: CredentialOwnerInput, overrides: Record = {}) { + return { + ...input, + tokenScope: 'team', + teamId: 'team-1', + projectId: 'project-1', + teamSlug: 'team-slug', + projectSlug: 'project-slug', + setupStatus: 'ready', + setupStep: null, + setupError: null, + buildGeneration: snapshot.buildGeneration, + runtimeBuildId: 'runtime-build-1', + runtimeSnapshotId: snapshot.runtimeSnapshotId, + setupStartedAt: null, + setupCompletedAt: null, + ...overrides, + }; +} + +function stubCredentialResponse(input: CredentialOwnerInput): void { + vi.stubGlobal( + 'fetch', + vi.fn(async () => Response.json(credentialResponse(input))) + ); +} + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('BYOC Vercel credential encryption', () => { + it('decrypts a dedicated keyed envelope bound to its organization and credential', async () => { + stubCredentialResponse({ ...identity, tokenEncrypted: encryptCredentialToken() }); + + await expect(resolveByocVercelCredentials(env, identity)).resolves.toEqual({ + accessToken, + teamId: 'team-1', + scope: 'team', + }); + }); + + it('prevents the ordinary encryptedSecrets path from decrypting extracted BYOC ciphertext', () => { + const envelope = encryptCredentialToken(); + + expect(() => + decryptSecrets({ STOLEN_BYOC_TOKEN: envelope.ciphertext }, keys.privateKey) + ).toThrow(); + }); + + it('decrypts a personal envelope with the user-scoped AAD', async () => { + stubCredentialResponse({ + ...personalIdentity, + tokenEncrypted: encryptCredentialToken(personalIdentity), + }); + + await expect(resolveByocVercelCredentials(env, personalIdentity)).resolves.toEqual({ + accessToken, + teamId: 'team-1', + scope: 'team', + }); + }); + + it('rejects a personal credential response when the owner changes', async () => { + stubCredentialResponse({ + userId: 'oauth/another-user', + credentialId: personalIdentity.credentialId, + tokenEncrypted: encryptCredentialToken(personalIdentity), + }); + + await expect(resolveByocVercelCredentials(env, personalIdentity)).rejects.toBeInstanceOf( + ByocCredentialResolverError + ); + }); + + it('rejects personal ciphertext encrypted with a different owner AAD', async () => { + stubCredentialResponse({ + ...personalIdentity, + tokenEncrypted: encryptCredentialToken({ + userId: 'oauth/another-user', + credentialId: personalIdentity.credentialId, + }), + }); + + await expect(resolveByocVercelCredentials(env, personalIdentity)).rejects.toBeInstanceOf( + ByocCredentialResolverError + ); + }); + + it('fetches personal credentials with an encoded user owner query', async () => { + stubCredentialResponse({ + ...personalIdentity, + tokenEncrypted: encryptCredentialToken(personalIdentity), + }); + const fetchMock = vi.mocked(globalThis.fetch); + + await fetchByocVercelCredential(env, personalIdentity); + + expect(fetchMock).toHaveBeenCalledWith( + `https://backend.example.test/api/internal/byoc/vercel-credentials/${personalIdentity.credentialId}?userId=${encodeURIComponent(personalIdentity.userId)}`, + expect.objectContaining({ method: 'GET' }) + ); + }); + + it('fetches personal enrollment from the user owner endpoint', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(async () => + Response.json({ + ...personalIdentity, + setupStatus: 'ready', + setupStep: null, + setupError: null, + buildGeneration: snapshot.buildGeneration, + runtimeBuildId: 'runtime-build-1', + runtimeSnapshotId: snapshot.runtimeSnapshotId, + teamSlug: 'team-slug', + projectSlug: 'project-slug', + }) + ) + ); + + await expect( + fetchByocVercelEnrollmentForUser(env, personalIdentity.userId) + ).resolves.toMatchObject(personalIdentity); + expect(globalThis.fetch).toHaveBeenCalledWith( + `https://backend.example.test/api/internal/byoc/vercel-credentials/user/${encodeURIComponent(personalIdentity.userId)}`, + expect.objectContaining({ method: 'GET' }) + ); + }); + + it.each([ + { + name: 'organization', + organizationId: '44444444-4444-4444-8444-444444444444', + credentialId: identity.credentialId, + }, + { + name: 'credential', + organizationId: identity.organizationId, + credentialId: '55555555-5555-4555-8555-555555555555', + }, + ])('rejects ciphertext replayed against a different $name', async input => { + stubCredentialResponse({ ...input, tokenEncrypted: encryptCredentialToken() }); + + await expect(resolveByocVercelCredentials(env, input)).rejects.toBeInstanceOf( + ByocCredentialResolverError + ); + }); + + it.each([ + { + name: 'ordinary encryptedSecrets envelope', + envelope: () => encryptWithPublicKey(accessToken, keys.publicKey), + }, + { + name: 'foreign envelope scheme', + envelope: () => ({ ...encryptCredentialToken(), scheme: 'foreign-credential-scheme' }), + }, + { + name: 'unknown key identifier', + envelope: () => ({ ...encryptCredentialToken(), keyId: 'foreign-key-v1' }), + }, + { + name: 'malformed ciphertext', + envelope: () => { + const encrypted = encryptCredentialToken(); + return { + ...encrypted, + ciphertext: { ...encrypted.ciphertext, encryptedData: '' }, + }; + }, + }, + ])('rejects an $name returned by the internal credential API', async ({ envelope }) => { + stubCredentialResponse({ ...identity, tokenEncrypted: envelope() }); + + await expect(fetchByocVercelCredential(env, identity)).rejects.toBeInstanceOf( + ByocCredentialResolverError + ); + }); + + it('exposes only the generation-fenced snapshot identity after resolving runtime credentials', async () => { + stubCredentialResponse({ ...identity, tokenEncrypted: encryptCredentialToken() }); + const onSnapshotResolved = vi.fn(); + + await expect( + resolveByocVercelRuntimeConfig( + { + ...env, + VERCEL_SANDBOX_RUNTIME: 'node24', + VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: '300000', + VERCEL_SANDBOX_EXTEND_DURATION_MS: '600000', + }, + identity, + onSnapshotResolved + ) + ).resolves.toMatchObject({ snapshotId: snapshot.runtimeSnapshotId, scope: 'team' }); + + expect(onSnapshotResolved).toHaveBeenCalledExactlyOnceWith(snapshot); + expect(JSON.stringify(onSnapshotResolved.mock.calls)).not.toContain(accessToken); + }); + + it('marks a credential without team metadata as project-scoped', async () => { + stubCredentialResponse({ + ...identity, + tokenEncrypted: encryptCredentialToken(), + }); + const fetchMock = vi.mocked(globalThis.fetch); + fetchMock.mockImplementationOnce(async () => + Response.json( + credentialResponse( + { ...identity, tokenEncrypted: encryptCredentialToken() }, + { tokenScope: 'project', teamSlug: null } + ) + ) + ); + + await expect(resolveByocVercelCredentials(env, identity)).resolves.toMatchObject({ + accessToken, + teamId: 'team-1', + scope: 'project', + }); + }); +}); + +describe('BYOC Vercel missing snapshot projection', () => { + it('marks the exact ready generation failed without exposing credentials and normalizes PostgreSQL timestamps', async () => { + const encrypted = encryptCredentialToken(); + const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { + if (init?.method === 'PATCH') return Response.json({ updated: true }); + return Response.json( + credentialResponse( + { ...identity, tokenEncrypted: encrypted }, + { + setupStartedAt: '2026-04-29 01:16:12.945+00', + setupCompletedAt: '2026-04-29 01:20:12.945+00', + } + ) + ); + }); + vi.stubGlobal('fetch', fetchMock); + + await expect(projectByocVercelSnapshotMissing(env, snapshot)).resolves.toBe(true); + + const update = fetchMock.mock.calls[1]?.[1]; + expect(update?.method).toBe('PATCH'); + expect(typeof update?.body).toBe('string'); + if (typeof update?.body !== 'string') throw new Error('Expected JSON projection'); + expect(JSON.parse(update.body)).toEqual({ + organizationId: identity.organizationId, + credentialId: identity.credentialId, + buildGeneration: snapshot.buildGeneration, + setupStatus: 'failed', + setupStep: null, + setupError: 'byoc_vercel_snapshot_missing', + teamSlug: 'team-slug', + projectSlug: 'project-slug', + runtimeBuildId: 'runtime-build-1', + runtimeSnapshotId: null, + setupStartedAt: '2026-04-29T01:16:12.945Z', + setupCompletedAt: null, + }); + expect(update.body).not.toContain(accessToken); + expect(update.body).not.toContain(encrypted.ciphertext.encryptedData); + }); + + it.each([ + { + name: 'newer generation', + overrides: { buildGeneration: '44444444-4444-4444-8444-444444444444' }, + }, + { name: 'replacement snapshot', overrides: { runtimeSnapshotId: 'snapshot-2' } }, + { name: 'already rebuilding setup', overrides: { setupStatus: 'building' } }, + ])('does not overwrite a $name observed before projection', async ({ overrides }) => { + const fetchMock = vi.fn(async () => + Response.json( + credentialResponse({ ...identity, tokenEncrypted: encryptCredentialToken() }, overrides) + ) + ); + vi.stubGlobal('fetch', fetchMock); + + await expect(projectByocVercelSnapshotMissing(env, snapshot)).resolves.toBe(false); + expect(fetchMock).toHaveBeenCalledOnce(); + }); + + it('preserves the original generation fence when setup changes after credential lookup', async () => { + const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => + init?.method === 'PATCH' + ? Response.json({ updated: false }) + : Response.json( + credentialResponse({ ...identity, tokenEncrypted: encryptCredentialToken() }) + ) + ); + vi.stubGlobal('fetch', fetchMock); + + await expect(projectByocVercelSnapshotMissing(env, snapshot)).resolves.toBe(false); + + const body = fetchMock.mock.calls[1]?.[1]?.body; + if (typeof body !== 'string') throw new Error('Expected fenced JSON projection'); + expect(JSON.parse(body)).toMatchObject({ buildGeneration: snapshot.buildGeneration }); + }); + + it('keeps a temporarily unavailable projection retryable', async () => { + const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => + init?.method === 'PATCH' + ? new Response(null, { status: 503 }) + : Response.json( + credentialResponse({ ...identity, tokenEncrypted: encryptCredentialToken() }) + ) + ); + vi.stubGlobal('fetch', fetchMock); + + await expect(projectByocVercelSnapshotMissing(env, snapshot)).rejects.toBeInstanceOf( + ByocCredentialResolverError + ); + }); +}); + +describe('internal credential request redirects', () => { + it.each(['credential', 'enrollment', 'projection'] as const)( + 'rejects a %s redirect without following it with the internal secret', + async operation => { + const fetchMock = vi + .fn() + .mockResolvedValue( + new Response(null, { status: 302, headers: { location: 'https://other.example.test' } }) + ); + vi.stubGlobal('fetch', fetchMock); + const request = + operation === 'credential' + ? fetchByocVercelCredential(env, identity) + : operation === 'enrollment' + ? fetchByocVercelEnrollmentForUser(env, personalIdentity.userId) + : projectByocVercelStatus(env, { + ...identity, + buildGeneration: snapshot.buildGeneration, + setupStatus: 'failed', + setupStep: null, + setupError: 'test', + teamSlug: null, + projectSlug: null, + runtimeBuildId: null, + runtimeSnapshotId: null, + setupStartedAt: null, + setupCompletedAt: null, + }); + await expect(request).rejects.toBeInstanceOf(ByocCredentialResolverError); + expect(fetchMock).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ redirect: 'manual' }) + ); + } + ); +}); diff --git a/services/cloud-agent-next/src/byoc/vercel-credential-resolver.ts b/services/cloud-agent-next/src/byoc/vercel-credential-resolver.ts new file mode 100644 index 00000000000..7ee14dc0fd8 --- /dev/null +++ b/services/cloud-agent-next/src/byoc/vercel-credential-resolver.ts @@ -0,0 +1,407 @@ +import { + byocVercelOwnerSchema, + byocVercelOwnerFields, + type ByocVercelOwner, +} from './vercel-owner.js'; +export type { ByocVercelOwner } from './vercel-owner.js'; +import { decryptKeyedEnvelope } from '@kilocode/encryption'; +import { + parseVercelSandboxRuntimeDefaults, + type VercelSandboxCredentials, + type VercelSandboxRuntimeConfig, +} from '../agent-sandbox/vercel/vercel-runtime-config.js'; +import type { + VercelComputeCredentialEnvelope, + VercelComputeSetupStatus, + VercelComputeSetupStep, +} from '@kilocode/db/schema-types'; +import type { Env } from '../types.js'; +import { z } from 'zod'; + +const BYOC_VERCEL_ENVELOPE_SCHEME = + 'byoc-vercel-credential-rsa-aes-256-gcm' satisfies VercelComputeCredentialEnvelope['scheme']; +const BYOC_VERCEL_ENVELOPE_KEY_ID = 'agent-env-vars-v1'; + +const EnvelopeSchema = z.object({ + scheme: z.literal(BYOC_VERCEL_ENVELOPE_SCHEME), + version: z.literal(1), + keyId: z.literal(BYOC_VERCEL_ENVELOPE_KEY_ID), + ciphertext: z.object({ + encryptedData: z.string().min(1), + encryptedDEK: z.string().min(1), + algorithm: z.literal('rsa-aes-256-gcm'), + version: z.literal(1), + }), +}); + +const SetupStepSchema = z.enum([ + 'validating_access', + 'create_builder', + 'install_system_dependencies', + 'install_node_dependencies', + 'upload_runtime_artifacts', + 'verify_runtime_artifacts', + 'snapshot_builder', + 'create_validator', + 'launch_validator_wrapper', + 'verify_validator_call_home', + 'stop_validator', + 'confirm_terminal', +]); + +const CredentialOwnerSchema = byocVercelOwnerSchema; + +const CredentialFieldsSchema = z.object({ + credentialId: z.uuid(), + tokenEncrypted: EnvelopeSchema, + tokenScope: z.enum(['team', 'project']), + teamId: z.string().min(1), + projectId: z.string().min(1), + teamSlug: z.string().nullable(), + projectSlug: z.string().nullable(), + setupStatus: z.enum(['pending', 'building', 'ready', 'failed']), + setupStep: SetupStepSchema.nullable(), + setupError: z.string().nullable(), + buildGeneration: z.uuid(), + runtimeBuildId: z.string().nullable(), + runtimeSnapshotId: z.string().nullable(), + setupStartedAt: z.string().nullable(), + setupCompletedAt: z.string().nullable(), +}); +const CredentialSchema = CredentialFieldsSchema.and(CredentialOwnerSchema); + +const EnrollmentFieldsSchema = z.object({ + credentialId: z.uuid(), + setupStatus: z.enum(['pending', 'building', 'ready', 'failed']), + setupStep: SetupStepSchema.nullable(), + setupError: z.string().nullable(), + buildGeneration: z.uuid(), + runtimeBuildId: z.string().nullable(), + runtimeSnapshotId: z.string().nullable(), + teamSlug: z.string().nullable(), + projectSlug: z.string().nullable(), +}); +const EnrollmentSchema = EnrollmentFieldsSchema.and(CredentialOwnerSchema); + +export type ByocVercelCredentialFetchInput = ByocVercelOwner & { credentialId: string }; + +export type ByocVercelCredential = ByocVercelOwner & + z.infer & { + tokenEncrypted: VercelComputeCredentialEnvelope; + setupStatus: VercelComputeSetupStatus; + setupStep: VercelComputeSetupStep | null; + }; + +export type ByocVercelEnrollment = ByocVercelOwner & z.infer; + +export type ByocVercelRuntimeSnapshot = ByocVercelOwner & { + credentialId: string; + buildGeneration: string; + runtimeSnapshotId: string; +}; + +export type ByocVercelStatusProjection = ByocVercelOwner & { + credentialId: string; + buildGeneration: string; + setupStatus: VercelComputeSetupStatus; + setupStep: VercelComputeSetupStep | null; + setupError: string | null; + teamSlug: string | null; + projectSlug: string | null; + runtimeBuildId: string | null; + runtimeSnapshotId: string | null; + setupStartedAt: string | null; + setupCompletedAt: string | null; + runtimeWrapperVersion?: string | null; + runtimeReleasedAt?: string | null; + runtimeDigest?: string | null; +}; + +export class ByocCredentialMissingError extends Error { + readonly code = 'byoc_credential_missing'; + + constructor(credentialId: string) { + super(`BYOC credential ${credentialId} is no longer available`); + this.name = 'ByocCredentialMissingError'; + } +} + +export class ByocVercelNotReadyError extends Error { + readonly code = 'byoc_vercel_not_ready'; + + constructor(status: VercelComputeSetupStatus) { + super(`BYOC Vercel setup is ${status}`); + this.name = 'ByocVercelNotReadyError'; + } +} + +export class ByocCredentialResolverError extends Error { + readonly code = 'byoc_credential_resolver_failed'; + + constructor() { + super('BYOC credential lookup failed'); + this.name = 'ByocCredentialResolverError'; + } +} + +function backendUrl(env: Env): string { + const value = env.KILOCODE_BACKEND_BASE_URL?.trim().replace(/\/+$/, ''); + if (!value) throw new ByocCredentialResolverError(); + return value; +} + +function ownerId(owner: ByocVercelOwner): string { + return owner.organizationId !== undefined ? owner.organizationId : owner.userId; +} + +function ownerAadKey(owner: ByocVercelOwner): string { + return owner.organizationId !== undefined ? owner.organizationId : `user:${owner.userId}`; +} + +function ownerMatches(left: ByocVercelOwner, right: ByocVercelOwner): boolean { + if (left.organizationId !== undefined && right.organizationId !== undefined) { + return left.organizationId === right.organizationId; + } + if (left.userId !== undefined && right.userId !== undefined) return left.userId === right.userId; + return false; +} + +async function internalSecret(env: Env): Promise { + try { + const secret = await env.INTERNAL_API_SECRET_PROD.get(); + if (secret) return secret; + } catch { + // Convert secret-store failures into the same safe resolver error. + } + throw new ByocCredentialResolverError(); +} + +export async function fetchByocVercelCredential( + env: Env, + input: ByocVercelCredentialFetchInput +): Promise { + const ownerQuery = + input.organizationId !== undefined + ? `organizationId=${encodeURIComponent(input.organizationId)}` + : `userId=${encodeURIComponent(input.userId)}`; + let response: Response; + try { + response = await fetch( + `${backendUrl(env)}/api/internal/byoc/vercel-credentials/${encodeURIComponent(input.credentialId)}?${ownerQuery}`, + { + method: 'GET', + headers: { 'x-internal-api-key': await internalSecret(env) }, + cache: 'no-store', + redirect: 'manual', + } + ); + } catch { + throw new ByocCredentialResolverError(); + } + + if (response.status === 404) { + throw new ByocCredentialMissingError(input.credentialId); + } + if (!response.ok) { + throw new ByocCredentialResolverError(); + } + + const parsed = CredentialSchema.safeParse(await response.json().catch(() => null)); + if (!parsed.success) throw new ByocCredentialResolverError(); + if (!ownerMatches(parsed.data, input) || parsed.data.credentialId !== input.credentialId) { + throw new ByocCredentialResolverError(); + } + return parsed.data; +} + +export async function fetchByocVercelEnrollment( + env: Env, + organizationId: string +): Promise { + return fetchByocVercelEnrollmentForOwner(env, { organizationId }, 'organization'); +} + +export async function fetchByocVercelEnrollmentForUser( + env: Env, + userId: string +): Promise { + return fetchByocVercelEnrollmentForOwner(env, { userId }, 'user'); +} + +async function fetchByocVercelEnrollmentForOwner( + env: Env, + owner: ByocVercelOwner, + path: 'organization' | 'user' +): Promise { + let response: Response; + try { + response = await fetch( + `${backendUrl(env)}/api/internal/byoc/vercel-credentials/${path}/${encodeURIComponent(ownerId(owner))}`, + { + method: 'GET', + headers: { 'x-internal-api-key': await internalSecret(env) }, + cache: 'no-store', + redirect: 'manual', + } + ); + } catch { + throw new ByocCredentialResolverError(); + } + + if (response.status === 404) { + throw new ByocCredentialMissingError(ownerId(owner)); + } + if (!response.ok) throw new ByocCredentialResolverError(); + + const parsed = EnrollmentSchema.safeParse(await response.json().catch(() => null)); + if (!parsed.success || !ownerMatches(parsed.data, owner)) { + throw new ByocCredentialResolverError(); + } + return parsed.data; +} + +export async function projectByocVercelStatus( + env: Env, + input: ByocVercelStatusProjection +): Promise { + let response: Response; + try { + response = await fetch( + `${backendUrl(env)}/api/internal/byoc/vercel-credentials/${encodeURIComponent(input.credentialId)}`, + { + method: 'PATCH', + headers: { + 'content-type': 'application/json', + 'x-internal-api-key': await internalSecret(env), + }, + body: JSON.stringify(input), + cache: 'no-store', + redirect: 'manual', + } + ); + } catch { + throw new ByocCredentialResolverError(); + } + + if (response.status === 404) return false; + if (!response.ok) throw new ByocCredentialResolverError(); + const parsed = z + .object({ updated: z.boolean() }) + .safeParse(await response.json().catch(() => null)); + if (!parsed.success) throw new ByocCredentialResolverError(); + return parsed.data.updated; +} + +export async function projectByocVercelSnapshotMissing( + env: Env, + input: ByocVercelRuntimeSnapshot +): Promise { + const credential = await fetchByocVercelCredential(env, input); + if ( + credential.setupStatus !== 'ready' || + credential.buildGeneration !== input.buildGeneration || + credential.runtimeSnapshotId !== input.runtimeSnapshotId + ) { + return false; + } + + return projectByocVercelStatus(env, { + ...byocVercelOwnerFields(credential), + credentialId: credential.credentialId, + buildGeneration: credential.buildGeneration, + setupStatus: 'failed', + setupStep: null, + setupError: 'byoc_vercel_snapshot_missing', + teamSlug: credential.teamSlug, + projectSlug: credential.projectSlug, + runtimeBuildId: credential.runtimeBuildId, + runtimeSnapshotId: null, + setupStartedAt: credential.setupStartedAt + ? new Date(credential.setupStartedAt).toISOString() + : null, + setupCompletedAt: null, + }); +} + +export async function resolveByocVercelRuntimeConfig( + env: Env, + input: ByocVercelCredentialFetchInput, + onSnapshotResolved?: (snapshot: ByocVercelRuntimeSnapshot) => void +): Promise { + const credential = await fetchByocVercelCredential(env, input); + if (credential.setupStatus !== 'ready' || !credential.runtimeSnapshotId) { + throw new ByocVercelNotReadyError(credential.setupStatus); + } + + const config = decryptCredentialRuntimeConfig(env, credential); + onSnapshotResolved?.({ + ...byocVercelOwnerFields(credential), + credentialId: credential.credentialId, + buildGeneration: credential.buildGeneration, + runtimeSnapshotId: credential.runtimeSnapshotId, + }); + return config; +} + +export async function resolveByocVercelCredentials( + env: Env, + input: ByocVercelCredentialFetchInput +): Promise { + const credential = await fetchByocVercelCredential(env, input); + return { + accessToken: decryptCredentialAccessToken(env, credential), + teamId: credential.teamId, + scope: credential.tokenScope, + }; +} + +export async function resolveByocVercelAccessConfig( + env: Env, + input: ByocVercelCredentialFetchInput +): Promise { + const credential = await fetchByocVercelCredential(env, input); + return decryptCredentialRuntimeConfig(env, credential); +} + +function decryptCredentialRuntimeConfig( + env: Env, + credential: ByocVercelCredential +): VercelSandboxRuntimeConfig { + if (!credential.runtimeBuildId) throw new ByocCredentialResolverError(); + + const defaults = parseVercelSandboxRuntimeDefaults(env); + if (!defaults || !env.AGENT_ENV_VARS_PRIVATE_KEY) { + throw new ByocCredentialResolverError(); + } + + return { + ...defaults, + accessToken: decryptCredentialAccessToken(env, credential), + teamId: credential.teamId, + scope: credential.tokenScope, + projectId: credential.projectId, + snapshotId: credential.runtimeSnapshotId ?? 'pending', + runtimeBuildId: credential.runtimeBuildId, + }; +} + +function decryptCredentialAccessToken(env: Env, credential: ByocVercelCredential): string { + if (!env.AGENT_ENV_VARS_PRIVATE_KEY) throw new ByocCredentialResolverError(); + try { + const accessToken = decryptKeyedEnvelope( + JSON.stringify(credential.tokenEncrypted), + BYOC_VERCEL_ENVELOPE_SCHEME, + { + active: { + keyId: BYOC_VERCEL_ENVELOPE_KEY_ID, + privateKeyPem: env.AGENT_ENV_VARS_PRIVATE_KEY, + }, + }, + `byoc-vercel-credential:v1:${ownerAadKey(credential)}:${credential.credentialId}` + ).trim(); + if (!accessToken) throw new Error('empty credential'); + return accessToken; + } catch { + throw new ByocCredentialResolverError(); + } +} diff --git a/services/cloud-agent-next/src/byoc/vercel-owner.test.ts b/services/cloud-agent-next/src/byoc/vercel-owner.test.ts new file mode 100644 index 00000000000..791b220a3f4 --- /dev/null +++ b/services/cloud-agent-next/src/byoc/vercel-owner.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest'; +import { enrolledByocVercelOwner, byocVercelOwnerSchema } from './vercel-owner.js'; +import { controlPlaneSandboxSelectionSchema } from '../control-plane/session/sandbox-selection.js'; + +const orgId = '11111111-1111-4111-8111-111111111111'; +const userId = 'oauth/google/personal-owner'; +const credentialId = '22222222-2222-4222-8222-222222222222'; + +describe('Vercel BYOC ownership', () => { + it('enrolls listed users and organizations through the same environment variable', () => { + const env = { BYOC_VERCEL_IDS: ` , ${userId}, ${orgId}, ` }; + expect(enrolledByocVercelOwner(env, { userId })).toEqual({ userId }); + expect(enrolledByocVercelOwner(env, { userId, orgId })).toEqual({ organizationId: orgId }); + expect(enrolledByocVercelOwner(env, { userId: 'another-user' })).toBeUndefined(); + expect( + enrolledByocVercelOwner(env, { + userId, + orgId: '33333333-3333-4333-8333-333333333333', + }) + ).toBeUndefined(); + }); + it('uses the organization owner whenever an organization is present', () => { + expect(enrolledByocVercelOwner({ BYOC_VERCEL_IDS: userId }, { userId, orgId })).toBeUndefined(); + expect(enrolledByocVercelOwner({ BYOC_VERCEL_IDS: orgId }, { userId })).toBeUndefined(); + expect(enrolledByocVercelOwner({ BYOC_VERCEL_IDS: orgId }, { userId, orgId })).toEqual({ + organizationId: orgId, + }); + }); + it('enrolls both owner types with a wildcard and neither with an empty list', () => { + expect(enrolledByocVercelOwner({ BYOC_VERCEL_IDS: '*' }, { userId })).toEqual({ userId }); + expect(enrolledByocVercelOwner({ BYOC_VERCEL_IDS: '*' }, { userId, orgId })).toEqual({ + organizationId: orgId, + }); + expect(enrolledByocVercelOwner({}, { userId })).toBeUndefined(); + expect(enrolledByocVercelOwner({}, { userId, orgId })).toBeUndefined(); + expect(enrolledByocVercelOwner({ BYOC_VERCEL_IDS: '*' }, {})).toBeUndefined(); + }); + it('requires exactly one owner and Vercel at the provider pin boundary', () => { + expect(controlPlaneSandboxSelectionSchema.safeParse({ provider: 'vercel' }).success).toBe( + false + ); + for (const owner of [{ userId }, { organizationId: orgId }]) { + expect( + controlPlaneSandboxSelectionSchema.safeParse({ + provider: 'vercel', + byoc: { ...owner, credentialId }, + }).success + ).toBe(true); + expect( + controlPlaneSandboxSelectionSchema.safeParse({ + provider: 'cloudflare', + byoc: { ...owner, credentialId }, + }).success + ).toBe(false); + } + for (const owner of [{}, { userId, organizationId: orgId }, { userId: '' }]) { + expect(byocVercelOwnerSchema.safeParse(owner).success).toBe(false); + expect( + controlPlaneSandboxSelectionSchema.safeParse({ + provider: 'vercel', + byoc: { ...owner, credentialId }, + }).success + ).toBe(false); + } + }); +}); diff --git a/services/cloud-agent-next/src/byoc/vercel-owner.ts b/services/cloud-agent-next/src/byoc/vercel-owner.ts new file mode 100644 index 00000000000..a5ecbef7bdd --- /dev/null +++ b/services/cloud-agent-next/src/byoc/vercel-owner.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +export const VERCEL_BYOC_REQUIRED_MESSAGE = + 'Kilo-paid Vercel compute is retired. Configure Vercel BYOC and start a new session.'; + +export const byocVercelOwnerSchema = z.union([ + z.object({ organizationId: z.uuid(), userId: z.never().optional() }), + z.object({ organizationId: z.never().optional(), userId: z.string().min(1) }), +]); +export type ByocVercelOwner = z.infer; + +export function byocVercelOwnerFields(owner: ByocVercelOwner): ByocVercelOwner { + return owner.organizationId !== undefined + ? { organizationId: owner.organizationId } + : { userId: owner.userId }; +} + +export function enrolledByocVercelOwner( + env: { BYOC_VERCEL_IDS?: string }, + owner: { userId?: string; orgId?: string } +): ByocVercelOwner | undefined { + const id = owner.orgId ?? owner.userId; + if (id === undefined) return undefined; + const enrolled = new Set( + (env.BYOC_VERCEL_IDS ?? '') + .split(',') + .map(value => value.trim()) + .filter(Boolean) + ); + if (!enrolled.has('*') && !enrolled.has(id)) return undefined; + return owner.orgId !== undefined ? { organizationId: owner.orgId } : { userId: id }; +} diff --git a/services/cloud-agent-next/src/byoc/vercel-provider.test.ts b/services/cloud-agent-next/src/byoc/vercel-provider.test.ts new file mode 100644 index 00000000000..3281de759ac --- /dev/null +++ b/services/cloud-agent-next/src/byoc/vercel-provider.test.ts @@ -0,0 +1,180 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Env } from '../types.js'; +import type * as CredentialResolverModule from './vercel-credential-resolver.js'; +import { + VERCEL_CLOUD_AGENT_CREATE_OPERATION_TAG, + VERCEL_CLOUD_AGENT_RESOURCE_TAG, + VERCEL_CLOUD_AGENT_RESOURCE_TAG_VALUE, + VERCEL_CLOUD_AGENT_RUNTIME_BUILD_TAG, +} from '../agent-sandbox/vercel/vercel-sandbox-rest-client.js'; +import { + createByocVercelProviderAdapter, + unavailableVercelProviderAdapter, +} from './vercel-provider.js'; +import { + resolveByocVercelRuntimeConfig, + resolveByocVercelAccessConfig, + projectByocVercelSnapshotMissing, +} from './vercel-credential-resolver.js'; + +vi.mock('./vercel-credential-resolver.js', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + resolveByocVercelRuntimeConfig: vi.fn(), + resolveByocVercelAccessConfig: vi.fn(), + projectByocVercelSnapshotMissing: vi.fn(), + }; +}); + +const source = { + organizationId: '11111111-1111-4111-8111-111111111111', + credentialId: '22222222-2222-4222-8222-222222222222', +}; +const snapshot = { + ...source, + buildGeneration: '33333333-3333-4333-8333-333333333333', + runtimeSnapshotId: 'snap_customer', +}; +const config = { + accessToken: 'customer-test-token', + teamId: 'team_customer', + projectId: 'prj_customer', + snapshotId: 'snap_customer', + runtimeBuildId: 'build_customer', + runtime: 'node24' as const, + initialTimeoutMs: 300_000, + extendDurationMs: 600_000, +}; +const intent = { intentId: 'create_customer', createdAt: 1 }; +const sandboxName = 'ses-customer'; + +function adapter() { + return createByocVercelProviderAdapter({ + env: Object.assign({} as Env, { + WORKER_URL: 'http://worker.test', + VERCEL_TOKEN: 'platform-test-token', + }), + source, + sandboxName, + readContainedGrants: async () => [], + }); +} + +function createdSandbox() { + return { + sandbox: { + name: sandboxName, + currentSessionId: 'sbox_customer', + status: 'running', + persistent: false, + createdAt: 1, + updatedAt: 1, + tags: { + [VERCEL_CLOUD_AGENT_RESOURCE_TAG]: VERCEL_CLOUD_AGENT_RESOURCE_TAG_VALUE, + [VERCEL_CLOUD_AGENT_CREATE_OPERATION_TAG]: intent.intentId, + [VERCEL_CLOUD_AGENT_RUNTIME_BUILD_TAG]: config.runtimeBuildId, + }, + }, + session: { + id: 'sbox_customer', + sourceSandboxName: sandboxName, + projectId: config.projectId, + sourceSnapshotId: config.snapshotId, + runtime: config.runtime, + status: 'running', + memory: 4096, + vcpus: 2, + region: 'iad1', + timeout: 300_000, + requestedAt: 1, + cwd: '/', + createdAt: 1, + updatedAt: 1, + }, + routes: [], + }; +} + +beforeEach(() => { + vi.mocked(resolveByocVercelRuntimeConfig) + .mockReset() + .mockImplementation(async (_env, input, resolved) => { + expect(input).toEqual(source); + resolved?.(snapshot); + return config; + }); + vi.mocked(resolveByocVercelAccessConfig).mockReset().mockResolvedValue(config); + vi.mocked(projectByocVercelSnapshotMissing).mockReset().mockResolvedValue(true); +}); +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('BYOC Vercel provider on the current control plane', () => { + it('creates with the customer token, project, and snapshot instead of platform credentials', async () => { + const requests: { url: string; authorization: string | null; body: unknown }[] = []; + vi.stubGlobal('fetch', async (url: string, init: RequestInit) => { + if (typeof init.body !== 'string') throw new Error('Expected a JSON request body'); + requests.push({ + url, + authorization: new Headers(init.headers).get('authorization'), + body: JSON.parse(init.body), + }); + return Response.json(createdSandbox()); + }); + expect(await adapter().create(intent)).toEqual({ + providerRef: JSON.stringify({ + sandboxName, + sessionId: 'sbox_customer', + metadata: { region: 'iad1', sourceSnapshotId: config.snapshotId, instanceType: 'small' }, + }), + }); + expect(requests).toHaveLength(1); + expect(requests[0]).toMatchObject({ + url: 'https://api.vercel.com/v2/sandboxes?teamId=team_customer', + authorization: 'Bearer customer-test-token', + body: { + projectId: 'prj_customer', + source: { type: 'snapshot', snapshotId: 'snap_customer' }, + }, + }); + }); + + it.each([404, 410])( + 'marks only the resolved customer snapshot missing after provider %s', + async status => { + vi.stubGlobal('fetch', async () => new Response('', { status })); + await expect(adapter().create(intent)).rejects.toMatchObject({ + reason: 'byoc_vercel_snapshot_missing', + }); + expect(projectByocVercelSnapshotMissing).toHaveBeenCalledWith(expect.anything(), snapshot); + } + ); + + it.each([ + [403, 'byoc_vercel_forbidden'], + [429, 'byoc_vercel_capacity'], + ])('projects provider status %s without invalidating the snapshot', async (status, reason) => { + vi.stubGlobal('fetch', async () => new Response('', { status: Number(status) })); + await expect(adapter().create(intent)).rejects.toMatchObject({ reason }); + expect(projectByocVercelSnapshotMissing).not.toHaveBeenCalled(); + }); +}); + +describe('retired Kilo-paid Vercel adapter', () => { + it('fails every compute operation without using customer or platform credentials', async () => { + const retired = unavailableVercelProviderAdapter(); + for (const operation of [ + () => retired.ensureBillingAdmission('retired'), + () => retired.create({ intentId: 'retired', createdAt: 1, allocationName: 'retired' }), + () => retired.launch('retired', { command: { argv: [] }, env: {} }), + () => retired.observe('retired'), + () => retired.stop('retired'), + () => retired.ensureLeaseAtLeast('retired', 1), + () => retired.logs('retired'), + ]) { + await expect(operation()).rejects.toMatchObject({ reason: 'byoc_credential_missing' }); + } + }); +}); diff --git a/services/cloud-agent-next/src/byoc/vercel-provider.ts b/services/cloud-agent-next/src/byoc/vercel-provider.ts new file mode 100644 index 00000000000..3f9adbc66f6 --- /dev/null +++ b/services/cloud-agent-next/src/byoc/vercel-provider.ts @@ -0,0 +1,111 @@ +import type { ByocVercelOwner } from './vercel-owner.js'; +import type { Env } from '../types.js'; +import type { ProviderAdapter } from '../sandbox-control/provider.js'; +import type { VercelSandboxResources } from '@kilocode/worker-utils/sandbox-allocation'; +import type { SessionCredentialGrant } from '../sandbox-control/session-credentials.js'; +import { + createVercelProviderAdapter, + type VercelProviderLocator, +} from '../sandbox-control/vercel-provider.js'; +import { VercelSandboxRestError } from '../agent-sandbox/vercel/vercel-sandbox-rest-client.js'; +import { + ByocCredentialMissingError, + ByocVercelNotReadyError, + resolveByocVercelAccessConfig, + resolveByocVercelRuntimeConfig, + projectByocVercelSnapshotMissing, + type ByocVercelRuntimeSnapshot, +} from './vercel-credential-resolver.js'; + +export type ByocVercelSource = ByocVercelOwner & { credentialId: string }; + +export class ByocVercelProviderError extends Error { + constructor( + readonly reason: + | 'byoc_credential_missing' + | 'byoc_vercel_not_ready' + | 'byoc_vercel_forbidden' + | 'byoc_vercel_capacity' + | 'byoc_vercel_snapshot_missing' + ) { + super(reason); + this.name = 'ByocVercelProviderError'; + } +} + +/** Resolve customer credentials for each operation. Never persist a decrypted token. */ +export function createByocVercelProviderAdapter(deps: { + env: Env; + source: ByocVercelSource; + sandboxName: string; + resources?: VercelSandboxResources; + locator?: VercelProviderLocator | null; + readContainedGrants: () => Promise; + readAcceptedRuntimeProxyHandles?: () => Promise>; +}): ProviderAdapter { + async function accessAdapter() { + const config = await resolveByocVercelAccessConfig(deps.env, deps.source); + return createVercelProviderAdapter({ + ...deps, + config: { ...config, ...deps.locator, resources: deps.resources }, + }); + } + + return { + ensureBillingAdmission: async () => {}, + async create(intent) { + let snapshot: ByocVercelRuntimeSnapshot | undefined; + try { + const config = await resolveByocVercelRuntimeConfig(deps.env, deps.source, value => { + snapshot = value; + }); + const adapter = createVercelProviderAdapter({ + ...deps, + config: { ...config, resources: deps.resources }, + }); + return await adapter.create(intent); + } catch (error) { + if (error instanceof ByocCredentialMissingError) + throw new ByocVercelProviderError(error.code); + if (error instanceof ByocVercelNotReadyError) throw new ByocVercelProviderError(error.code); + if (error instanceof VercelSandboxRestError) { + if (error.status === 403 || error.status === 401) + throw new ByocVercelProviderError('byoc_vercel_forbidden'); + if (error.status === 429) throw new ByocVercelProviderError('byoc_vercel_capacity'); + if ((error.status === 404 || error.status === 410) && snapshot) { + await projectByocVercelSnapshotMissing(deps.env, snapshot); + throw new ByocVercelProviderError('byoc_vercel_snapshot_missing'); + } + } + throw error; + } + }, + launch: async (ref, env, options) => (await accessAdapter()).launch(ref, env, options), + observe: async (ref, intent) => (await accessAdapter()).observe(ref, intent), + stop: async (ref, intent) => (await accessAdapter()).stop(ref, intent), + ensureLeaseAtLeast: async (ref, ms) => (await accessAdapter()).ensureLeaseAtLeast(ref, ms), + logs: async ref => (await accessAdapter()).logs(ref), + async applyContainedCredentials(ref, grants) { + const adapter = await accessAdapter(); + if (!adapter.applyContainedCredentials) + throw new Error('Vercel credential containment is unavailable'); + await adapter.applyContainedCredentials(ref, grants); + }, + }; +} + +/** A retired platform pin remains readable but has no compute authority. */ +export function unavailableVercelProviderAdapter(): ProviderAdapter { + const unavailable = async (): Promise => { + throw new ByocVercelProviderError('byoc_credential_missing'); + }; + return { + ensureBillingAdmission: unavailable, + create: unavailable, + launch: unavailable, + observe: unavailable, + stop: unavailable, + ensureLeaseAtLeast: unavailable, + logs: unavailable, + }; +} diff --git a/services/cloud-agent-next/src/byoc/vercel-runtime-artifacts.ts b/services/cloud-agent-next/src/byoc/vercel-runtime-artifacts.ts new file mode 100644 index 00000000000..d3f2e525853 --- /dev/null +++ b/services/cloud-agent-next/src/byoc/vercel-runtime-artifacts.ts @@ -0,0 +1,42 @@ +// These files are bundled as Wrangler Data modules. The wrapper build runs in +// the Worker package's predeploy hook, so the DO never depends on an operator's +// local filesystem at runtime. +import controlWrapperArtifact from '../../wrapper/dist/control-plane-wrapper.js?binary'; +import supervisorArtifact from '../../wrapper/control-plane-supervisor.sh?binary'; + +export type RuntimeArtifact = { + path: string; + bytes: Uint8Array; + sha256: string; +}; + +function artifactBytes(value: unknown): Uint8Array { + if (typeof value === 'string') return new TextEncoder().encode(value); + if (value instanceof ArrayBuffer) return new Uint8Array(value); + if (value instanceof Uint8Array) return value; + throw new Error('Runtime artifact was not bundled as binary data'); +} + +async function sha256(bytes: Uint8Array): Promise { + const digest = await crypto.subtle.digest('SHA-256', bytes); + return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join(''); +} + +export async function getVercelRuntimeArtifacts(): Promise { + const artifacts = [ + { + path: 'usr/local/bin/kilocode-control-plane-wrapper.js', + bytes: artifactBytes(controlWrapperArtifact), + }, + { + path: 'usr/local/bin/kilocode-control-plane-supervisor.sh', + bytes: artifactBytes(supervisorArtifact), + }, + ]; + return Promise.all( + artifacts.map(async artifact => ({ + ...artifact, + sha256: await sha256(artifact.bytes), + })) + ); +} diff --git a/services/cloud-agent-next/src/byoc/vercel-runtime-base.ts b/services/cloud-agent-next/src/byoc/vercel-runtime-base.ts new file mode 100644 index 00000000000..28885e89e6d --- /dev/null +++ b/services/cloud-agent-next/src/byoc/vercel-runtime-base.ts @@ -0,0 +1,20 @@ +import { RUNTIME_BASE_ABI } from '../sandbox-control/runtime-release.js'; +import { RUNTIME_DISTRIBUTION } from '../shared/runtime-distribution.js'; + +export const VERCEL_BASE_SYSTEM_SETUP = + 'set -euo pipefail; sudo dnf install -y git git-lfs jq tar gzip; sudo mkdir -p /workspace /usr/local/share/kilo /opt/git/etc; sudo chown "$(id -u):$(id -g)" /workspace; test -w /workspace; sudo git lfs install --system --skip-repo'; + +export const VERCEL_BASE_NODE_SETUP = `set -euo pipefail; node_path="$(node -p 'process.execPath')"; sudo ln -sf "$node_path" /usr/local/bin/node; test -x /usr/local/bin/node; curl -fsSL https://bun.sh/install | bash -s bun-v${RUNTIME_DISTRIBUTION.bun}; sudo install -m 0755 "$HOME/.bun/bin/bun" /usr/local/bin/bun`; + +/** Downloaded releases replace CLI/control files; only base dependencies require a new snapshot. */ +export async function vercelRuntimeBaseDigest(): Promise { + const input = JSON.stringify({ + format: 1, + abi: RUNTIME_BASE_ABI, + nodeMajor: RUNTIME_DISTRIBUTION.nodeMajor, + systemSetup: VERCEL_BASE_SYSTEM_SETUP, + bunSetup: VERCEL_BASE_NODE_SETUP, + }); + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input)); + return `downloaded-base-v1:${Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('')}`; +} diff --git a/services/cloud-agent-next/src/byoc/vercel-runtime-identity.test.ts b/services/cloud-agent-next/src/byoc/vercel-runtime-identity.test.ts new file mode 100644 index 00000000000..5dc59654ed0 --- /dev/null +++ b/services/cloud-agent-next/src/byoc/vercel-runtime-identity.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('./vercel-runtime-artifacts.js', () => ({ + getVercelRuntimeArtifacts: vi.fn(), +})); + +import { + VERCEL_RUNTIME_BUN_VERSION, + VERCEL_RUNTIME_KILO_CLI_VERSION, + vercelRuntimeDigest, + getVercelRuntimeIdentity, +} from './vercel-runtime-identity.js'; +import { getVercelRuntimeArtifacts } from './vercel-runtime-artifacts.js'; + +describe('vercelRuntimeDigest', () => { + it('uses the same base identity across downloaded CLI and control releases', async () => { + const first = { RUNTIME_STARTUP_MODE: 'downloaded', RUNTIME_RELEASE_DESCRIPTOR: 'release-a' }; + const second = { ...first, RUNTIME_RELEASE_DESCRIPTOR: 'release-b' }; + const identity = await getVercelRuntimeIdentity(first); + expect(await getVercelRuntimeIdentity(second)).toEqual(identity); + expect(identity.digest).toMatch(/^downloaded-base-v1:[a-f0-9]{64}$/); + expect(identity.releasedAt).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(getVercelRuntimeArtifacts).not.toHaveBeenCalled(); + }); + it('changes when the control-plane supervisor changes', () => { + const base = { + wrapperVersion: '2.4.0', + releasedAt: '2026-09-08', + controlWrapperSha256: 'bb', + kiloCliVersion: VERCEL_RUNTIME_KILO_CLI_VERSION, + bunVersion: VERCEL_RUNTIME_BUN_VERSION, + }; + expect(vercelRuntimeDigest({ ...base, supervisorSha256: 'cc' })).not.toBe( + vercelRuntimeDigest({ ...base, supervisorSha256: 'dd' }) + ); + }); + it('changes when the control wrapper hash changes', () => { + const base = { + wrapperVersion: '2.4.0', + releasedAt: '2026-09-08', + controlWrapperSha256: 'bb', + kiloCliVersion: VERCEL_RUNTIME_KILO_CLI_VERSION, + bunVersion: VERCEL_RUNTIME_BUN_VERSION, + }; + expect(vercelRuntimeDigest({ ...base, controlWrapperSha256: 'cc' })).not.toBe( + vercelRuntimeDigest(base) + ); + }); +}); diff --git a/services/cloud-agent-next/src/byoc/vercel-runtime-identity.ts b/services/cloud-agent-next/src/byoc/vercel-runtime-identity.ts new file mode 100644 index 00000000000..c907b75366e --- /dev/null +++ b/services/cloud-agent-next/src/byoc/vercel-runtime-identity.ts @@ -0,0 +1,66 @@ +import { KILO_CLI_VERSION } from '../shared/kilo-cli-version.js'; +import { RUNTIME_DISTRIBUTION } from '../shared/runtime-distribution.js'; +import { WRAPPER_RELEASED_AT, WRAPPER_VERSION } from '../shared/wrapper-version.js'; +import { getVercelRuntimeArtifacts } from './vercel-runtime-artifacts.js'; +import { vercelRuntimeBaseDigest } from './vercel-runtime-base.js'; +import type { Env } from '../types.js'; + +export const VERCEL_RUNTIME_BUN_VERSION = RUNTIME_DISTRIBUTION.bun; +export const VERCEL_RUNTIME_KILO_CLI_VERSION = KILO_CLI_VERSION; + +export type VercelRuntimeIdentity = { + wrapperVersion: string; + releasedAt: string; + digest: string; +}; + +export function vercelRuntimeDigest(input: { + wrapperVersion: string; + releasedAt: string; + controlWrapperSha256: string; + kiloCliVersion: string; + bunVersion: string; + supervisorSha256?: string; +}): string { + return [ + input.wrapperVersion, + input.releasedAt, + input.controlWrapperSha256, + `cli:${input.kiloCliVersion}`, + `bun:${input.bunVersion}`, + ...(input.supervisorSha256 ? [input.supervisorSha256] : []), + ].join('|'); +} + +export async function getVercelRuntimeIdentity( + env: Pick = {} +): Promise { + if (env.RUNTIME_STARTUP_MODE === 'downloaded') { + return { + wrapperVersion: 'downloaded-base-v1', + releasedAt: '2026-10-23', + digest: await vercelRuntimeBaseDigest(), + }; + } + const artifacts = await getVercelRuntimeArtifacts(); + const controlWrapper = artifacts.find( + artifact => artifact.path === 'usr/local/bin/kilocode-control-plane-wrapper.js' + ); + const supervisor = artifacts.find( + artifact => artifact.path === 'usr/local/bin/kilocode-control-plane-supervisor.sh' + ); + if (!controlWrapper || !supervisor) throw new Error('runtime_artifact_missing'); + + return { + wrapperVersion: WRAPPER_VERSION, + releasedAt: WRAPPER_RELEASED_AT, + digest: vercelRuntimeDigest({ + wrapperVersion: WRAPPER_VERSION, + releasedAt: WRAPPER_RELEASED_AT, + controlWrapperSha256: controlWrapper.sha256, + supervisorSha256: supervisor.sha256, + kiloCliVersion: VERCEL_RUNTIME_KILO_CLI_VERSION, + bunVersion: VERCEL_RUNTIME_BUN_VERSION, + }), + }; +} diff --git a/services/cloud-agent-next/src/byoc/vercel-snapshot-build-stub.ts b/services/cloud-agent-next/src/byoc/vercel-snapshot-build-stub.ts new file mode 100644 index 00000000000..311897478e5 --- /dev/null +++ b/services/cloud-agent-next/src/byoc/vercel-snapshot-build-stub.ts @@ -0,0 +1,10 @@ +import type { VercelSnapshotBuild } from '../persistence/VercelSnapshotBuild.js'; +import type { Env } from '../types.js'; + +export function getVercelSnapshotBuildStub( + env: Pick, + ownerKey: string +): DurableObjectStub { + if (!env.VERCEL_SNAPSHOT_BUILD) throw new Error('Vercel snapshot build binding is unavailable'); + return env.VERCEL_SNAPSHOT_BUILD.getByName(ownerKey); +} diff --git a/services/cloud-agent-next/src/byoc/vercel-snapshot-owner.ts b/services/cloud-agent-next/src/byoc/vercel-snapshot-owner.ts new file mode 100644 index 00000000000..3f5571477fb --- /dev/null +++ b/services/cloud-agent-next/src/byoc/vercel-snapshot-owner.ts @@ -0,0 +1,48 @@ +import { z } from 'zod'; +import type { ByocVercelOwner } from './vercel-owner.js'; + +export type SnapshotBuildOwner = + | { type: 'org'; organizationId: string } + | { type: 'user'; userId: string }; + +export function ownerToRowKey(owner: SnapshotBuildOwner): string { + return owner.type === 'org' ? owner.organizationId : `user:${owner.userId}`; +} + +export function ownerFromRow(row: { + organization_id: string; + owner_type: string; +}): SnapshotBuildOwner { + const ownerType = z.enum(['org', 'user']).parse(row.owner_type); + if (ownerType === 'org') { + if (row.organization_id.startsWith('user:')) { + throw new Error('snapshot_owner_row_key_mismatch'); + } + return { type: 'org', organizationId: row.organization_id }; + } + if (!row.organization_id.startsWith('user:') || row.organization_id.length <= 'user:'.length) { + throw new Error('snapshot_owner_row_key_mismatch'); + } + return { type: 'user', userId: row.organization_id.slice('user:'.length) }; +} + +export function ownerToProjection( + owner: SnapshotBuildOwner +): { organizationId: string } | { userId: string } { + return owner.type === 'org' ? { organizationId: owner.organizationId } : { userId: owner.userId }; +} + +export function ownerFromInput(input: ByocVercelOwner): SnapshotBuildOwner { + return input.organizationId !== undefined + ? { type: 'org', organizationId: input.organizationId } + : { type: 'user', userId: input.userId }; +} + +export function ownerCredentialFetchInput( + owner: SnapshotBuildOwner, + credentialId: string +): { organizationId: string; credentialId: string } | { userId: string; credentialId: string } { + return owner.type === 'org' + ? { organizationId: owner.organizationId, credentialId } + : { userId: owner.userId, credentialId }; +} diff --git a/services/cloud-agent-next/src/control-plane/sandbox/sandbox-do.ts b/services/cloud-agent-next/src/control-plane/sandbox/sandbox-do.ts index f8e705de099..3b3457c796b 100644 --- a/services/cloud-agent-next/src/control-plane/sandbox/sandbox-do.ts +++ b/services/cloud-agent-next/src/control-plane/sandbox/sandbox-do.ts @@ -13,9 +13,7 @@ import { DurableObject } from 'cloudflare:workers'; import { getSandbox, type Sandbox } from '@cloudflare/sandbox'; import { z } from 'zod'; import { DEFAULT_DO_RETRY_CONFIG, withTimeout } from '@kilocode/worker-utils'; -import { getBillingContext } from '@kilocode/container-usage'; import { egressPolicySchema, type EgressMode } from '@kilocode/worker-utils/egress-allowlist'; -import type { VercelSandboxResources } from '@kilocode/worker-utils/sandbox-allocation'; import { CloudAgentRunContainerSchema } from '@kilocode/worker-utils/cloud-agent-queue-report'; import { eq, sql } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/durable-sqlite'; @@ -26,13 +24,10 @@ import { type SandboxBillingInput, } from '../../container-usage-context.js'; import { resolveSecret } from '../../auth.js'; -import { isCloudAgentContainerBillingEnabled } from '../../container-billing-rollout.js'; import { assertContainerCapacity, getContainerCapacityFailureReason, } from '../../container-concurrency.js'; -import { BillingScheduleTable } from '../../sandbox-control/billing-schedule.js'; -import { createVercelBillingRuntime, type VercelBillingRuntime } from './vercel-billing-runtime.js'; import { generateSandboxCredential, hashSandboxCredential, @@ -59,7 +54,6 @@ import { type SandboxProviderConfiguration, } from '../../sandbox-control/provider.js'; import { - createVercelProviderAdapter, vercelProviderLocatorSchema, type VercelProviderLocator, } from '../../sandbox-control/vercel-provider.js'; @@ -81,7 +75,6 @@ import { type SessionCredentialGrant, } from '../../sandbox-control/session-credentials.js'; import { providerUsesOutboundCredentialProxy } from '../../agent-sandbox/capabilities.js'; -import { resolveVercelSandboxRuntimeConfig } from '../../agent-sandbox/vercel/vercel-runtime-config.js'; import { getManagedOutboundContainerId } from '../../sandbox-id.js'; import { resolveSandboxNamespace, @@ -306,6 +299,18 @@ import type { WorktreeChangesReady } from '../worktree-changes-ready.js'; import type { WorktreeChangesContext } from '../../sandbox-session/worktree-changes.js'; import { buildNativeEgressAllowlist } from '../../sandbox-control/egress-policy.js'; import { nativeEgressAllowlistSchema } from '../../sandbox-containers/egress-policy.js'; +import { + createByocVercelProviderAdapter, + unavailableVercelProviderAdapter, + ByocVercelProviderError, + type ByocVercelSource, +} from '../../byoc/vercel-provider.js'; +import { + SNAPSHOT_VALIDATOR_REF_KEY, + snapshotValidatorAllocation, + snapshotValidatorInputSchema, + type SnapshotValidatorInput, +} from './snapshot-validator.js'; const GENERATION_KEY = 'control_plane_generation'; const GENERATION = 2; @@ -316,6 +321,7 @@ const REPOSITORY_CAPTURE_CALL_MS = 5 * 60_000 + 5_000; const ALLOCATION_ROW_ID = 'current'; export type StoredProviderPin = { + byoc?: ByocVercelSource; provider: AgentSandboxProvider; allocationName: string | null; configuration: SandboxProviderConfiguration | null; @@ -328,6 +334,7 @@ export type StoredProviderPin = { }; export type EnsureAllocationInput = { + byoc?: ByocVercelSource; provider?: AgentSandboxProvider; allocationName?: string; configuration?: SandboxProviderConfiguration; @@ -635,9 +642,6 @@ export class SandboxControlV2 extends DurableObject { onSnapshot: (context, revision) => this.ctx.waitUntil(this.notifyWorktreeChangesReady(context, revision)), }); - private readonly billingSchedule: BillingScheduleTable; - /** Vercel metered-billing runtime; reads live pin/storage, never frozen config. */ - private readonly billingRuntime: VercelBillingRuntime; /** * Single-flight canonical handle mints, keyed by scope and current fence. Two * concurrent preparers of the same scope wait on one Session DO mint so only @@ -690,28 +694,6 @@ export class SandboxControlV2 extends DurableObject { super(ctx, env); this.sandboxId = ctx.id.name ?? ctx.id.toString(); this.db = drizzle(ctx.storage, { logger: false }); - this.billingSchedule = new BillingScheduleTable({ - storage: ctx.storage, - recompose: async () => this.armAlarm(await this.readAllocation()), - }); - this.billingRuntime = createVercelBillingRuntime({ - storage: ctx.storage, - env: () => this.env, - sandboxId: this.sandboxId, - schedule: this.billingSchedule, - currentProvider: () => this.currentProvider(), - currentPin: () => this.providerPin, - readAllocation: () => this.readAllocation(), - requireOwner: () => this.requireOwner(), - isCreateInFlight: () => this.createInFlight, - provider: () => this.provider, - timers: () => this.sandboxTimers(), - stopRef: (ref, allocationId) => this.stopRef(ref, allocationId), - failUnconfirmedCleanup: state => this.failUnconfirmedVercelCleanup(state), - requestStop: () => - this.dispatchResult({ type: 'stop-requested', at: Date.now(), reason: 'billing_blocked' }), - waitUntil: promise => this.ctx.waitUntil(promise), - }); this.provider = this.createProviderAdapter(this.defaultPin('cloudflare')); this.initialized = ctx.blockConcurrencyWhile(() => this.initializeStorage()); } @@ -739,6 +721,73 @@ export class SandboxControlV2 extends DurableObject { }); } + async initializeSnapshotValidator(input: SnapshotValidatorInput): Promise { + await this.initialized; + const parsed = snapshotValidatorInputSchema.parse(input); + await this.enqueue(async () => { + const adopted = snapshotValidatorAllocation(this.sandboxId, parsed, Date.now()); + const registered = await this.ctx.storage.get(SNAPSHOT_VALIDATOR_REF_KEY); + const state = await this.readAllocation(); + if (registered !== undefined) { + if ( + registered !== parsed.allocation.providerRef || + state.providerRef !== registered || + JSON.stringify(this.providerPin) !== JSON.stringify(adopted.pin) + ) { + throw new Error('Snapshot validator allocation changed'); + } + if ((await this.ctx.storage.get(CREDENTIAL_HASH_KEY)) === parsed.credentialHash) return; + if (this.ctx.getWebSockets().length > 0) + throw new Error('Snapshot validator already connected'); + await this.ctx.storage.put(CREDENTIAL_HASH_KEY, parsed.credentialHash); + return; + } + if ( + state.kind !== 'stopped' || + state.unconfirmedProviderRef !== null || + this.providerPin?.byoc !== undefined || + (await this.requireOwner()) !== null + ) { + throw new Error('Snapshot validator control is unavailable'); + } + await this.ctx.storage.transaction(async () => { + await this.writeProviderPin(adopted.pin); + await this.writeAllocation(adopted.state); + await this.ctx.storage.put({ + [SNAPSHOT_VALIDATOR_REF_KEY]: parsed.allocation.providerRef, + [CREDENTIAL_HASH_KEY]: parsed.credentialHash, + }); + }); + this.providerPin = adopted.pin; + this.provider = this.createProviderAdapter(adopted.pin); + await this.armAlarm(adopted.state); + }); + } + + async getSnapshotValidatorStatus(): Promise<{ physical: string; connection: string }> { + await this.initialized; + if (!(await this.ctx.storage.get(SNAPSHOT_VALIDATOR_REF_KEY))) + throw new Error('Snapshot validator is not registered'); + const state = await this.readAllocation(); + return { + physical: state.kind === 'stopped' ? 'stopped' : 'running', + connection: state.kind === 'connected' ? 'ready' : 'connecting', + }; + } + + async confirmSnapshotValidatorStopped(providerRef: string): Promise { + await this.initialized; + await this.enqueue(async () => { + if (!/^ses-byoc-validator-[0-9a-f]{32}$/i.test(this.sandboxId)) + throw new Error('Snapshot validator identity mismatch'); + const registered = await this.ctx.storage.get(SNAPSHOT_VALIDATOR_REF_KEY); + const state = await this.readAllocation(); + if (registered === undefined && state.kind === 'stopped') return; + if (registered !== providerRef) throw new Error('Snapshot validator allocation changed'); + await this.applyEvent({ type: 'provider-gone', at: Date.now() }); + }); + } + /** B3/D3 reports the provider no longer has the sandbox (spec §6 "any → stopped"). */ async reportProviderGone(): Promise { await this.initialized; @@ -1864,17 +1913,11 @@ export class SandboxControlV2 extends DurableObject { if (state.kind === 'creating' && this.createInFlight) { // N2: the in-flight attempt owns the create deadline. Keep a backstop; // it will dispatch its own result and re-arm. - for (const entry of await this.billingSchedule.dueEntries()) { - if (typeof entry.payload === 'string') { - await this.deferVercelBilling(entry.payload, entry.callback); - } - } await this.ctx.storage.setAlarm( Math.min( Date.now() + this.sandboxTimers().providerCreateMs, (await earliestRouteDeadlineAt(this.db)) ?? Infinity, - this.earliestHelloDeadlineAt() ?? Infinity, - this.billingSchedule.snapshotEarliestDue() ?? Infinity + this.earliestHelloDeadlineAt() ?? Infinity ) ); return; @@ -1885,20 +1928,6 @@ export class SandboxControlV2 extends DurableObject { nextAllocationId: crypto.randomUUID(), retryAllowed: await this.retryAllowed(), }); - try { - await withTimeout( - this.runBillingAlarm(), - this.sandboxTimers().providerStopAttemptMs, - 'Sandbox billing alarm timed out' - ); - } catch { - for (const entry of await this.billingSchedule.dueEntries()) { - if (typeof entry.payload === 'string') { - await this.deferVercelBilling(entry.payload, entry.callback); - } - } - logger.withFields({ sandboxId: this.sandboxId }).warn('Vercel billing alarm deferred'); - } await this.armAlarm(await this.readAllocation()); }); } @@ -1917,7 +1946,6 @@ export class SandboxControlV2 extends DurableObject { await this.ctx.storage.put(GENERATION_KEY, GENERATION); await migrate(this.db, migrations); for (const [key, value] of retainedBilling) await this.ctx.storage.put(key, value); - await this.billingSchedule.load(); const pin = this.cutoverPin(cutover); this.providerPin = pin; this.provider = this.createProviderAdapter(pin); @@ -1939,7 +1967,6 @@ export class SandboxControlV2 extends DurableObject { return; } await migrate(this.db, migrations); - await this.billingSchedule.load(); const row = await this.readAllocationRow(); if (row === null) return; const pin = (await this.readProviderPin()) ?? this.defaultPin('cloudflare'); @@ -2045,12 +2072,15 @@ export class SandboxControlV2 extends DurableObject { ) { throw new ProviderCreationError('invalid_configuration'); } + if (input.byoc && provider !== 'vercel') throw new Error('BYOC requires Vercel'); + if (provider === 'vercel' && !input.byoc) + throw new ByocVercelProviderError('byoc_credential_missing'); return { provider, allocationName: input.allocationName ?? this.sandboxId, configuration: input.configuration ?? { provider }, locator: input.locator ?? null, - billing: input.billing ?? null, + billing: input.byoc ? null : (input.billing ?? null), containment: input.containment ?? null, ...(input.sandboxKind ? { sandboxKind: input.sandboxKind } : {}), ...(policy.data.egressMode !== 'allowlist' @@ -2063,6 +2093,7 @@ export class SandboxControlV2 extends DurableObject { kiloToken, }), }), + ...(input.byoc ? { byoc: input.byoc } : {}), }; } @@ -2141,6 +2172,22 @@ export class SandboxControlV2 extends DurableObject { } } const { state, effects, stopReason } = reduceAllocation(previous, event, timers); + if ( + previous.kind === 'stopped' && + state.kind === 'creating' && + state.allocationId !== null && + this.providerPin?.provider === 'vercel' + ) { + // Vercel retains stopped names. Pin a unique physical name before create, + // so retries and reconstruction address this allocation's exact resource. + const pin = { + ...this.providerPin, + allocationName: `sbx-${state.allocationId.replaceAll('-', '')}`, + }; + await this.writeProviderPin(pin); + this.providerPin = pin; + this.provider = this.createProviderAdapter(pin); + } await this.writeAllocation(state, previous); if ( previous.kind !== state.kind || @@ -2161,7 +2208,6 @@ export class SandboxControlV2 extends DurableObject { }); } await this.armAlarm(state); - await this.afterVercelBillingTransition(event); await this.applyRouteEffects(previous, state, event, stopReason); for (const [sessionId, capture] of this.repositoryCaptures) { if (capture.allocationId !== state.allocationId || state.kind === 'stopping') { @@ -2231,6 +2277,12 @@ export class SandboxControlV2 extends DurableObject { // a later prepare for a different owner is rejected, not silently accepted. await this.initializeOwner(source.userId); let state = await this.readAllocation(); + if ( + (sandboxSelection?.provider ?? this.currentProvider()) === 'vercel' && + (sandboxSelection?.byoc ?? this.providerPin?.byoc) === undefined + ) { + return { state: 'failed', attemptId: crypto.randomUUID(), reason: 'byoc_credential_missing' }; + } // A worktree mid-deletion must never be re-prepared: a send in the Worker's // begin->finish window would otherwise start a fresh allocation and recreate // the checkout that deleteWorktreeResources just removed (spec §6, Shared @@ -2294,65 +2346,6 @@ export class SandboxControlV2 extends DurableObject { this.providerPin = pin; this.provider = this.createProviderAdapter(pin); } - if ( - state.kind !== 'stopped' && - state.kind !== 'creating' && - this.currentProvider() === 'vercel' && - (this.providerPin?.billing?.enforcementRequested || - isCloudAgentContainerBillingEnabled(this.env, { - userId: source.userId, - ...(this.providerPin?.billing?.subject.type === 'org' - ? { orgId: this.providerPin.billing.subject.id } - : {}), - })) - ) { - const context = await getBillingContext(this.ctx.storage); - const runtime = await this.ensureVercelBillingRuntime(); - const lostBillingGeneration = context === undefined || context.pendingStop !== undefined; - const billingReason: ControlPlaneFailureReason = - runtime !== undefined && (await runtime.lifecycle.isBillingBlocked()) - ? 'billing_blocked' - : lostBillingGeneration - ? 'sandbox_lost' - : 'billing_unavailable'; - let cannotContinue = - context === undefined || - runtime === undefined || - context.pendingStop !== undefined || - billingReason === 'billing_blocked'; - if (!cannotContinue && runtime !== undefined && context !== undefined) { - try { - await withTimeout( - runtime.lifecycle.ensureStartAcknowledged(context), - this.sandboxTimers().providerStopAttemptMs, - 'Sandbox billing admission timed out' - ); - } catch { - cannotContinue = true; - } - } - if (cannotContinue) { - if (state.kind !== 'stopping') { - await this.applyEvent({ - type: 'stop-requested', - at: Date.now(), - reason: billingReason, - }); - state = await this.readAllocation(); - } - if (billingReason === 'billing_blocked') { - const attemptId = crypto.randomUUID(); - const route = await failRoute( - this.routeContext(state), - { ...spec, attemptId }, - attemptId, - billingReason, - 'billing_admission' - ); - return { state: 'failed', attemptId: route.attemptId, reason: billingReason }; - } - } - } const repoKey = await this.repoKeyFor(spec, source); const { route, started } = await ensureRoute(this.routeContext(state), spec, source, repoKey); if (route.state !== 'failed') { @@ -4216,49 +4209,6 @@ export class SandboxControlV2 extends DurableObject { }); } - if (pin.provider === 'vercel') { - let admission: 'admitted' | 'blocked' | 'unavailable' | 'retry' = 'retry'; - let admissionRejected = false; - let priorGeneration: string | undefined; - let admissionStarted = false; - try { - priorGeneration = (await getBillingContext(this.ctx.storage))?.generation; - admissionStarted = true; - admission = await withTimeout( - this.admitVercelCreate(pin).catch((error: unknown) => { - admissionRejected = true; - throw error; - }), - Math.min(remainingMs(createDeadline), this.sandboxTimers().providerStopAttemptMs), - 'Vercel billing admission timed out' - ); - } catch { - let safeShadow = false; - if (priorGeneration === undefined) { - try { - safeShadow = await this.admitShadowAfterVercelBillingException(pin); - } catch { - safeShadow = false; - } - } - admission = safeShadow - ? 'admitted' - : admissionStarted && !admissionRejected - ? 'retry' - : 'unavailable'; - logger - .withFields({ sandboxId: this.sandboxId }) - .warn('Vercel billing admission failed'); - } - if (admission !== 'admitted') { - await this.failCreationRoutes( - allocationId, - admission === 'retry', - admission === 'blocked' ? 'billing_blocked' : 'billing_unavailable' - ); - return; - } - } const intent: ProviderCreateIntent = { intentId: allocationId, createdAt: Date.now(), @@ -4309,9 +4259,7 @@ export class SandboxControlV2 extends DurableObject { }); const current = await this.readAllocation(); if (current.kind !== 'creating' || current.allocationId !== allocationId) { - const confirmed = await this.stopRef(createdRef, allocationId); - if (pin.provider === 'vercel') - await this.settleStoppedCreatedVercelRef(createdRef, confirmed); + await this.stopRef(createdRef, allocationId); return; } const launchDeadline = current.createDeadlineAt ?? createDeadline; @@ -4423,6 +4371,10 @@ export class SandboxControlV2 extends DurableObject { providerRef: createdRef, }); } catch (error) { + if (error instanceof ByocVercelProviderError) { + await this.failCreationRoutes(allocationId, false, error.reason); + return; + } logControlDiagnostic( 'create_failed', { @@ -4450,9 +4402,7 @@ export class SandboxControlV2 extends DurableObject { if (createdRef !== null) { const latest = await this.readAllocation(); if (latest.kind === 'creating' && latest.allocationId === allocationId) { - const confirmed = await this.stopRef(createdRef, allocationId); - if (this.currentProvider() === 'vercel') - await this.settleStoppedCreatedVercelRef(createdRef, confirmed); + await this.stopRef(createdRef, allocationId); } } await this.dispatchCreateFailed(allocationId); @@ -4604,38 +4554,6 @@ export class SandboxControlV2 extends DurableObject { return result === 'terminal'; } - private async settleStoppedCreatedVercelRef(ref: string, confirmed: boolean): Promise { - await this.billingRuntime.settleStoppedCreatedRef(ref, confirmed); - } - - /** - * Fail-closed cleanup for an unconfirmed Vercel stop: fails the preparing - * routes and the creating allocation. Runs inside the alarm's serial queue, - * so it must not enqueue. - */ - private async failUnconfirmedVercelCleanup(state: AllocationState): Promise { - for (const route of await listRoutes(this.db)) { - if (route.state === 'preparing') { - await failCurrentAttempt( - this.routeContext(state), - route.sessionId, - route.attemptId, - 'sandbox_lost', - 'cleanup_unconfirmed' - ); - } - } - if (state.kind === 'creating' && state.allocationId !== null) { - await this.applyEvent({ - type: 'create-failed', - at: Date.now(), - allocationId: state.allocationId, - nextAllocationId: crypto.randomUUID(), - retryAllowed: false, - }); - } - } - private async runStop(stopAttempt: number): Promise { const state = await this.readAllocation(); if (state.kind !== 'stopping' || state.stopAttempt !== stopAttempt || state.stopPending) return; @@ -4810,9 +4728,6 @@ export class SandboxControlV2 extends DurableObject { } private async readWrapperBillingBlock(state: AllocationState): Promise { - if (this.currentProvider() === 'vercel') { - return (await this.ensureVercelBillingRuntime())?.lifecycle.isBillingBlocked() ?? false; - } return (await this.provider.isBillingBlocked?.(state.providerRef)) ?? false; } @@ -5167,6 +5082,8 @@ export class SandboxControlV2 extends DurableObject { logControlDiagnostic('hello_admission', { ...admission, accepted: false, reason, ...extra }); return false; }; + if (this.currentProvider() === 'vercel' && this.providerPin?.byoc === undefined) + return reject('byoc_credential_missing'); if (state.kind === 'stopped' || state.kind === 'stopping') { return reject(`allocation_${state.kind}`); } @@ -5219,16 +5136,6 @@ export class SandboxControlV2 extends DurableObject { }; } - private async admitVercelCreate( - pin: StoredProviderPin - ): Promise<'admitted' | 'blocked' | 'unavailable' | 'retry'> { - return this.billingRuntime.admitCreate(pin); - } - - private async admitShadowAfterVercelBillingException(pin: StoredProviderPin): Promise { - return this.billingRuntime.admitShadow(pin); - } - private async failCreationRoutes( allocationId: string, retryable: boolean, @@ -5264,47 +5171,27 @@ export class SandboxControlV2 extends DurableObject { }); } - private async recordVercelBillingLifetime(evidence: { - providerRef: string; - createdAtMs?: number; - terminalAtMs?: number; - }): Promise { - await this.billingRuntime.recordLifetime(evidence); - } - - private async afterVercelBillingTransition(event: AllocationEvent): Promise { - await this.billingRuntime.afterTransition(event); - } - - private async runBillingAlarm(): Promise { - await this.billingRuntime.runAlarm(); - } - - private async deferVercelBilling(generation: string, callback: string): Promise { - await this.billingRuntime.defer(generation, callback); - } - - private ensureVercelBillingRuntime() { - return this.billingRuntime.ensureRuntime(); - } - private createProviderAdapter(pin: StoredProviderPin): ProviderAdapter { const allocationName = pin.allocationName ?? this.sandboxId; switch (pin.provider) { case 'vercel': { const resources = pin.configuration?.provider === 'vercel' ? pin.configuration.resources : undefined; - const config = this.vercelConfig(pin.locator, resources); - return createVercelProviderAdapter({ - sandboxName: allocationName, - config, - billingLifetimeSink: evidence => this.recordVercelBillingLifetime(evidence), - // Authoritative live grants: read at create and on every credential - // refresh, never frozen into the adapter at construction. - readContainedGrants: () => this.activeGrants(Date.now()), - readAcceptedRuntimeProxyHandles: () => - listAcceptedHandleStringsByScopeId(this.ctx.storage, Date.now()), - }); + if (pin.byoc) { + return createByocVercelProviderAdapter({ + env: this.env, + source: pin.byoc, + sandboxName: allocationName, + resources, + locator: pin.locator, + // Authoritative live grants: read at create and on every credential + // refresh, never frozen into the adapter at construction. + readContainedGrants: () => this.activeGrants(Date.now()), + readAcceptedRuntimeProxyHandles: () => + listAcceptedHandleStringsByScopeId(this.ctx.storage, Date.now()), + }); + } + return unavailableVercelProviderAdapter(); } case 'cloudflare-containers': { const instance = @@ -5336,23 +5223,6 @@ export class SandboxControlV2 extends DurableObject { } } - private vercelConfig( - locator: VercelProviderLocator | null, - resources: VercelSandboxResources | undefined - ) { - if (locator === null) { - return resolveVercelSandboxRuntimeConfig(this.env, { resources }); - } - const resolved = resolveVercelSandboxRuntimeConfig(this.env, { - projectId: locator.projectId, - snapshotId: locator.snapshotId, - runtimeBuildId: locator.runtimeBuildId, - runtime: locator.runtime, - ...(resources === undefined ? {} : { resources }), - }); - return resolved === undefined ? undefined : { ...resolved, teamId: locator.teamId }; - } - private async retryAllowed(): Promise { return routeRetryAllowed(this.routeContext(await this.readAllocation())); } @@ -5490,15 +5360,9 @@ export class SandboxControlV2 extends DurableObject { (await routeRetryAllowed(this.routeContext(state))) ? Date.now() + this.sandboxTimers().providerCreateRetryMs : null; - if (this.billingSchedule.snapshotEarliestDue() === undefined) await this.billingSchedule.load(); - const billingAt = this.billingSchedule.snapshotEarliestDue() ?? null; - const candidates = [ - allocationAt, - routeAt, - retryAt, - billingAt, - this.earliestHelloDeadlineAt(), - ].filter((at): at is number => at !== null); + const candidates = [allocationAt, routeAt, retryAt, this.earliestHelloDeadlineAt()].filter( + (at): at is number => at !== null + ); if (candidates.length === 0) { await this.ctx.storage.deleteAlarm(); return; diff --git a/services/cloud-agent-next/src/control-plane/sandbox/snapshot-validator.ts b/services/cloud-agent-next/src/control-plane/sandbox/snapshot-validator.ts new file mode 100644 index 00000000000..de4c82df6a8 --- /dev/null +++ b/services/cloud-agent-next/src/control-plane/sandbox/snapshot-validator.ts @@ -0,0 +1,59 @@ +import { byocVercelOwnerSchema, byocVercelOwnerFields } from '../../byoc/vercel-owner.js'; +import { z } from 'zod'; +import { + decodeVercelProviderRef, + vercelProviderLocatorSchema, +} from '../../sandbox-control/vercel-provider.js'; +import { initialAllocationState, type AllocationState } from './allocation.js'; +import type { StoredProviderPin } from './sandbox-do.js'; + +export const SNAPSHOT_VALIDATOR_REF_KEY = 'byoc_snapshot_validator_ref'; +export const snapshotValidatorInputSchema = z + .object({ + build: z.object({ credentialId: z.uuid(), generation: z.uuid() }).and(byocVercelOwnerSchema), + allocation: z + .object({ + providerRef: z.string().min(1), + locator: vercelProviderLocatorSchema, + createdAt: z.number().finite(), + expiresAt: z.number().finite(), + }) + .strict(), + credentialHash: z.string().regex(/^[a-f0-9]{64}$/), + }) + .strict(); +export type SnapshotValidatorInput = z.infer; + +/** Adopt only the external sandbox identified by this build generation. */ +export function snapshotValidatorAllocation( + sandboxId: string, + input: SnapshotValidatorInput, + now: number +): { pin: StoredProviderPin; state: AllocationState } { + const reference = decodeVercelProviderRef(input.allocation.providerRef); + if ( + sandboxId !== `ses-byoc-validator-${input.build.generation.replaceAll('-', '')}` || + !reference || + input.allocation.expiresAt <= Math.max(now, input.allocation.createdAt) + ) { + throw new Error('Snapshot validator identity mismatch'); + } + return { + pin: { + provider: 'vercel', + allocationName: reference.sandboxName, + configuration: { provider: 'vercel' }, + locator: input.allocation.locator, + billing: null, + containment: null, + byoc: { ...byocVercelOwnerFields(input.build), credentialId: input.build.credentialId }, + }, + state: { + ...initialAllocationState(), + kind: 'starting', + allocationId: input.build.generation, + providerRef: input.allocation.providerRef, + firstConnectDeadlineAt: input.allocation.expiresAt, + }, + }; +} diff --git a/services/cloud-agent-next/src/control-plane/sandbox/vercel-billing-runtime.ts b/services/cloud-agent-next/src/control-plane/sandbox/vercel-billing-runtime.ts deleted file mode 100644 index 85089505572..00000000000 --- a/services/cloud-agent-next/src/control-plane/sandbox/vercel-billing-runtime.ts +++ /dev/null @@ -1,571 +0,0 @@ -import { - clearBillingContext, - ContainerUsageAdmissionError, - createContainerUsageClient, - DEFAULT_BILLING_HEARTBEAT_SECONDS, - getBillingContext, - installBillingHeartbeat, - type BillingHeartbeatController, -} from '@kilocode/container-usage'; -import { withTimeout } from '@kilocode/worker-utils'; -import type { VercelSandboxResources } from '@kilocode/worker-utils/sandbox-allocation'; -import { - assertSandboxBillingAllocation, - vercelBillingIdentity, -} from '../../container-usage-context.js'; -import { isCloudAgentContainerBillingEnabled } from '../../container-billing-rollout.js'; -import { MeteredBillingLifecycle, type BillingIdentity } from '../../metered-billing-lifecycle.js'; -import type { BillingScheduleTable } from '../../sandbox-control/billing-schedule.js'; -import { - VercelBilling, - VERCEL_BILLING_SETTLEMENT_CALLBACK, - deleteVercelBillingBinding, - loadVercelBillingBinding, - saveVercelBillingBinding, -} from '../../sandbox-control/vercel-billing.js'; -import { decodeVercelProviderRef } from '../../sandbox-control/vercel-provider.js'; -import type { ProviderAdapter } from '../../sandbox-control/provider.js'; -import type { AgentSandboxProvider, Env } from '../../types.js'; -import type { AllocationEvent, AllocationState, SandboxTimers } from './allocation.js'; -import type { StoredProviderPin } from './sandbox-do.js'; - -export const VERCEL_BILLING_FORCE_STOP_CALLBACK = 'billingForceStop'; -export const VERCEL_BILLING_DELIVERY_RETRY_MS = 5_000; - -export type VercelBillingLifetimeEvidence = { - providerRef: string; - createdAtMs?: number; - terminalAtMs?: number; -}; - -type VercelBillingRuntimeState = { - identity: BillingIdentity | undefined; - lifecycle: MeteredBillingLifecycle; - heartbeat: BillingHeartbeatController; - billing: VercelBilling; -}; - -/** - * Authoritative accessors. Every value is read live from the Durable Object's - * current pin, allocation and storage, so a helper built before an allocation - * exists (or before eviction restores the pin) still describes the current - * sandbox rather than constructor-frozen defaults. - */ -export type VercelBillingRuntimeDeps = { - storage: DurableObjectStorage; - env: () => Env; - sandboxId: string; - schedule: BillingScheduleTable; - currentProvider: () => AgentSandboxProvider; - currentPin: () => StoredProviderPin | null; - readAllocation: () => Promise; - requireOwner: () => Promise; - isCreateInFlight: () => boolean; - provider: () => ProviderAdapter; - timers: () => SandboxTimers; - /** Bounded provider stop that reports a confirmed terminal stop. */ - stopRef: (ref: string, allocationId: string | null) => Promise; - /** - * Fail-closed cleanup path for an unconfirmed Vercel stop: fails the - * preparing routes and the creating allocation. Owned by the DO because it - * mutates the allocation/reducer state. - */ - failUnconfirmedCleanup: (state: AllocationState) => Promise; - /** - * Commit a billing stop request to the allocation. Resolves after the - * request is durable, not after the sandbox stops. - */ - requestStop: () => Promise; - waitUntil: (promise: Promise) => void; -}; - -export type VercelBillingRuntime = { - ensureRuntime: () => Promise; - admitCreate: ( - pin: StoredProviderPin - ) => Promise<'admitted' | 'blocked' | 'unavailable' | 'retry'>; - admitShadow: (pin: StoredProviderPin) => Promise; - recordLifetime: (evidence: VercelBillingLifetimeEvidence) => Promise; - settleStoppedCreatedRef: (ref: string, confirmed: boolean) => Promise; - afterTransition: (event: AllocationEvent) => Promise; - prepareSettlement: (generation: string) => Promise; - runAlarm: () => Promise; - defer: (generation: string, callback: string) => Promise; -}; - -/** - * Vercel metered-billing runtime: construction and caching of the lifecycle, - * heartbeat and settlement owner, admission and shadow fallback, lifetime - * evidence, settlement scheduling and the billing alarm body. All durable - * state reads and the stop/fail-closed decisions are delegated back to the - * Durable Object through `deps`; this module never touches the allocation - * reducer or re-arms the alarm itself. - */ -export function createVercelBillingRuntime(deps: VercelBillingRuntimeDeps): VercelBillingRuntime { - let runtime: VercelBillingRuntimeState | undefined; - let build: Promise | undefined; - const deliveriesInFlight = new Set(); - - function vercelResources(): VercelSandboxResources | undefined { - const configuration = deps.currentPin()?.configuration; - return configuration?.provider === 'vercel' ? configuration.resources : undefined; - } - - async function containerState(): Promise<{ status: string; lastChange?: number }> { - const context = await getBillingContext(deps.storage); - if (context === undefined) return { status: 'running' }; - const binding = await loadVercelBillingBinding(deps.storage, context.generation); - if (binding?.terminalAtMs !== undefined) - return { status: 'stopped', lastChange: binding.terminalAtMs }; - if (binding === undefined || deps.currentProvider() !== 'vercel') return { status: 'running' }; - try { - const observed = await deps.provider().observe(binding.providerRef); - if (observed.status !== 'terminal') return { status: 'running' }; - const refreshed = await loadVercelBillingBinding(deps.storage, context.generation); - return { - status: 'stopped', - lastChange: refreshed?.terminalAtMs ?? context.usageMeasuredAtMs ?? binding.createdAtMs, - }; - } catch { - return { status: 'running' }; - } - } - - async function buildRuntime(): Promise { - const resources = vercelResources(); - const vercelIdentity = resources === undefined ? undefined : vercelBillingIdentity(resources); - const service = vercelIdentity?.service ?? (await getBillingContext(deps.storage))?.service; - if (service === undefined) return; - const identity: BillingIdentity | undefined = - vercelIdentity === undefined ? undefined : { sandboxClassName: vercelIdentity.className }; - const usageClient = createContainerUsageClient(deps.env().CONTAINER_USAGE_METER, { service }); - const schedule = (delaySeconds: number, callback: string, payload?: unknown) => - deps.schedule.schedule(callback, Date.now() + delaySeconds * 1_000, payload); - const deleteSchedules = (callback: string) => { - if (callback !== VERCEL_BILLING_SETTLEMENT_CALLBACK) - void deps.schedule.remove(callback).catch(() => undefined); - }; - const getState = () => containerState(); - const stopContainer = async () => { - const generation = (await getBillingContext(deps.storage))?.generation; - deps.waitUntil( - deps.requestStop().catch(async () => { - if (generation !== undefined) { - await deps.schedule.schedule( - VERCEL_BILLING_FORCE_STOP_CALLBACK, - Date.now() + VERCEL_BILLING_DELIVERY_RETRY_MS, - generation - ); - } - }) - ); - }; - const lifecycle = new MeteredBillingLifecycle({ - storage: deps.storage, - usageClient, - schedule, - deleteSchedules, - getState, - isContainerRunning: () => false, - stopContainer, - destroyContainer: stopContainer, - durableObjectId: deps.sandboxId, - waitUntil: promise => deps.waitUntil(promise), - }); - const heartbeat = installBillingHeartbeat( - { schedule, deleteSchedules, getState } as unknown as Parameters< - typeof installBillingHeartbeat - >[0], - { - client: usageClient, - storage: deps.storage, - stopOnStoppedState: false, - deferBudgetStopFinalSettlement: true, - beforeHeartbeatDelivery: context => lifecycle.ensureStartAcknowledged(context), - beforeStopDelivery: context => lifecycle.ensureStartAcknowledged(context), - onGenerationClosed: context => runtime?.billing.onGenerationClosed(context), - enforceBudgetStop: async (budget, expected) => { - if (identity === undefined) throw new Error('Vercel billing identity is unavailable'); - await lifecycle.enforceBudgetStop(identity, budget, expected); - }, - onBudgetWarning: async budget => { - if (identity !== undefined) await lifecycle.onBudgetWarning(identity, budget); - }, - } - ); - lifecycle.attachHeartbeat(heartbeat); - runtime = { - identity, - lifecycle, - heartbeat, - billing: new VercelBilling({ - storage: deps.storage, - lifecycle, - heartbeat, - schedule: deps.schedule, - }), - }; - } - - async function ensureRuntime(): Promise { - if (runtime !== undefined && (await getBillingContext(deps.storage)) === undefined) { - const resources = vercelResources(); - const className = - resources === undefined ? undefined : vercelBillingIdentity(resources).className; - if (runtime.identity?.sandboxClassName !== className) runtime = undefined; - } - if (runtime !== undefined) return runtime; - if (build !== undefined) { - await build; - return runtime; - } - const started = buildRuntime(); - build = started; - try { - await started; - } finally { - if (build === started) build = undefined; - } - return runtime; - } - - async function prepareSettlement(generation: string): Promise { - const active = await ensureRuntime(); - if (active === undefined) return; - await active.billing.prepareSettlement({ generation }); - } - - async function admitCreate( - pin: StoredProviderPin - ): Promise<'admitted' | 'blocked' | 'unavailable' | 'retry'> { - const existing = await getBillingContext(deps.storage); - if (existing !== undefined) { - await deps.schedule.ensure( - VERCEL_BILLING_SETTLEMENT_CALLBACK, - Date.now() + DEFAULT_BILLING_HEARTBEAT_SECONDS * 1_000, - existing.generation - ); - await prepareSettlement(existing.generation); - return 'retry'; - } - const billing = pin.billing; - const owner = await deps.requireOwner(); - if (billing === null) { - return owner !== null && !isCloudAgentContainerBillingEnabled(deps.env(), { userId: owner }) - ? 'admitted' - : 'unavailable'; - } - if (billing.sandboxId !== deps.sandboxId) return 'unavailable'; - if ( - owner === null || - (billing.subject.type === 'user' && billing.subject.id !== owner) || - (billing.actor.type === 'user' && billing.actor.id !== owner) - ) - return 'unavailable'; - const enforced = - billing.enforcementRequested === true || - isCloudAgentContainerBillingEnabled(deps.env(), { - userId: owner, - ...(billing.subject.type === 'org' ? { orgId: billing.subject.id } : {}), - }); - const active = await ensureRuntime(); - if (active?.identity === undefined) return 'unavailable'; - try { - assertSandboxBillingAllocation(active.identity.sandboxClassName, billing); - } catch { - return 'unavailable'; - } - const outcome = await active.lifecycle.openIntervalBeforeCreate(active.identity, { - ...billing, - enforcementRequested: enforced, - }); - await deps.schedule.ensure( - VERCEL_BILLING_SETTLEMENT_CALLBACK, - Date.now() + DEFAULT_BILLING_HEARTBEAT_SECONDS * 1_000, - outcome.generation - ); - if (outcome.kind === 'acked') return 'admitted'; - if (outcome.kind === 'definite_rejection') { - await clearBillingContext(deps.storage); - await deleteVercelBillingBinding(deps.storage, outcome.generation); - await deps.schedule.remove(VERCEL_BILLING_SETTLEMENT_CALLBACK, outcome.generation); - return enforced && - outcome.error instanceof ContainerUsageAdmissionError && - outcome.error.code === 'insufficient_credits' - ? 'blocked' - : enforced - ? 'unavailable' - : 'admitted'; - } - if (!enforced) return 'admitted'; - await prepareSettlement(outcome.generation); - return 'retry'; - } - - async function admitShadow(pin: StoredProviderPin): Promise { - const billing = pin.billing; - const owner = await deps.requireOwner(); - const resources = - pin.configuration?.provider === 'vercel' ? pin.configuration.resources : undefined; - if ( - billing === null || - owner === null || - resources === undefined || - billing.enforcementRequested || - billing.sandboxId !== deps.sandboxId || - (billing.subject.type === 'user' && billing.subject.id !== owner) || - (billing.actor.type === 'user' && billing.actor.id !== owner) || - isCloudAgentContainerBillingEnabled(deps.env(), { - userId: owner, - ...(billing.subject.type === 'org' ? { orgId: billing.subject.id } : {}), - }) - ) - return false; - let identity: ReturnType; - try { - identity = vercelBillingIdentity(resources); - assertSandboxBillingAllocation(identity.className, billing); - } catch { - return false; - } - const context = await getBillingContext(deps.storage); - if ( - context === undefined || - context.pendingStop !== undefined || - context.measurementStarted || - context.instanceId !== deps.sandboxId || - context.service !== identity.service || - context.sku !== identity.sku || - context.subject.type !== billing.subject.type || - context.subject.id !== billing.subject.id || - context.actor.type !== billing.actor.type || - context.actor.id !== billing.actor.id || - (await loadVercelBillingBinding(deps.storage, context.generation)) !== undefined - ) - return false; - await deps.schedule.ensure( - VERCEL_BILLING_SETTLEMENT_CALLBACK, - Date.now() + DEFAULT_BILLING_HEARTBEAT_SECONDS * 1_000, - context.generation - ); - return true; - } - - async function recordLifetime(evidence: VercelBillingLifetimeEvidence): Promise { - const context = await getBillingContext(deps.storage); - if (context === undefined) return; - const existing = await loadVercelBillingBinding(deps.storage, context.generation); - if (existing !== undefined && existing.providerRef !== evidence.providerRef) return; - const allocation = await deps.readAllocation(); - if (allocation.providerRef !== null) { - if (allocation.providerRef !== evidence.providerRef) return; - } else { - if ( - allocation.kind === 'stopped' || - existing !== undefined || - evidence.createdAtMs === undefined - ) - return; - const decoded = decodeVercelProviderRef(evidence.providerRef); - if (decoded?.sandboxName !== (deps.currentPin()?.allocationName ?? deps.sandboxId)) return; - } - const createdAtMs = evidence.createdAtMs ?? existing?.createdAtMs; - if (createdAtMs === undefined) return; - const terminalAtMs = evidence.terminalAtMs ?? existing?.terminalAtMs; - if (existing?.createdAtMs === createdAtMs && existing.terminalAtMs === terminalAtMs) return; - await saveVercelBillingBinding(deps.storage, { - generation: context.generation, - providerRef: evidence.providerRef, - createdAtMs, - ...(terminalAtMs === undefined ? {} : { terminalAtMs }), - }); - if (terminalAtMs !== undefined) await prepareSettlement(context.generation); - } - - async function settleStoppedCreatedRef(ref: string, confirmed: boolean): Promise { - const context = await getBillingContext(deps.storage); - if (context === undefined) return; - const binding = await loadVercelBillingBinding(deps.storage, context.generation); - if (binding?.providerRef !== ref) return; - if (!confirmed && binding.terminalAtMs === undefined) { - const allocation = await deps.readAllocation(); - await deps.schedule.schedule( - VERCEL_BILLING_SETTLEMENT_CALLBACK, - Date.now() + - (allocation.kind === 'stopped' - ? DEFAULT_BILLING_HEARTBEAT_SECONDS * 1_000 - : VERCEL_BILLING_DELIVERY_RETRY_MS), - context.generation - ); - return; - } - const terminalAtMs = - binding.terminalAtMs ?? Math.max(binding.createdAtMs, context.usageMeasuredAtMs); - if (binding.terminalAtMs === undefined) { - await saveVercelBillingBinding(deps.storage, { ...binding, terminalAtMs }); - } - await ( - await ensureRuntime() - )?.heartbeat.persistStop({ reason: 'runtime_signal' }, terminalAtMs); - await prepareSettlement(context.generation); - } - - async function afterTransition(event: AllocationEvent): Promise { - const context = await getBillingContext(deps.storage); - if (context === undefined) return; - const binding = await loadVercelBillingBinding(deps.storage, context.generation); - const state = await deps.readAllocation(); - if ( - state.kind === 'stopped' && - binding !== undefined && - binding.terminalAtMs === undefined && - !context.pendingStop && - event.type !== 'provider-gone' && - !(event.type === 'stop-result' && event.confirmed) - ) { - await deps.schedule.schedule( - VERCEL_BILLING_SETTLEMENT_CALLBACK, - Date.now() + VERCEL_BILLING_DELIVERY_RETRY_MS, - context.generation - ); - return; - } - if (state.kind === 'stopped' && !context.pendingStop) { - await ( - await ensureRuntime() - )?.heartbeat.persistStop( - { reason: 'runtime_signal' }, - binding?.terminalAtMs ?? context.usageMeasuredAtMs - ); - } - if (context.pendingStop || binding?.terminalAtMs !== undefined || state.kind === 'stopped') { - await prepareSettlement(context.generation); - } else if ( - binding !== undefined && - !context.measurementStarted && - (state.kind === 'starting' || state.kind === 'connected' || state.kind === 'disconnected') - ) { - await ( - await ensureRuntime() - )?.lifecycle.pinMeasurementCursor(context.generation, binding.createdAtMs); - } - } - - async function runAlarm(): Promise { - for (const entry of await deps.schedule.dueEntries()) { - const generation = typeof entry.payload === 'string' ? entry.payload : null; - if (generation === null) { - await deps.schedule.completeDue(entry); - continue; - } - const context = await getBillingContext(deps.storage); - if (context?.generation !== generation) { - await deps.schedule.completeDue(entry); - await deleteVercelBillingBinding(deps.storage, generation); - continue; - } - const active = await ensureRuntime(); - if (active === undefined) { - await defer(generation, entry.callback); - continue; - } - if (entry.callback === VERCEL_BILLING_FORCE_STOP_CALLBACK) { - if (active.identity === undefined) { - await defer(generation, entry.callback); - continue; - } - try { - await active.lifecycle.billingForceStop(active.identity, generation); - if ((await deps.readAllocation()).kind === 'stopped') { - await deps.schedule.completeDue(entry); - } else { - await defer(generation, entry.callback); - } - } catch { - await defer(generation, entry.callback); - } - continue; - } - if (entry.callback !== VERCEL_BILLING_SETTLEMENT_CALLBACK) { - await deps.schedule.completeDue(entry); - continue; - } - const binding = await loadVercelBillingBinding(deps.storage, generation); - const state = await deps.readAllocation(); - if ( - (state.kind === 'stopped' || (state.kind === 'creating' && !deps.isCreateInFlight())) && - binding !== undefined && - binding.terminalAtMs === undefined && - !context.pendingStop - ) { - const observation = await withTimeout( - deps.provider().observe(binding.providerRef), - deps.timers().providerStopAttemptMs, - 'Sandbox billing cleanup observation timed out' - ).catch(() => ({ status: 'unknown' as const })); - const confirmed = - observation.status === 'terminal' || - (await deps.stopRef(binding.providerRef, state.allocationId)); - await settleStoppedCreatedRef(binding.providerRef, confirmed); - if (!confirmed && Date.now() >= context.startEpochMs + deps.timers().providerCreateMs) { - await deps.failUnconfirmedCleanup(state); - } - continue; - } - if ( - context.pendingStop || - binding?.terminalAtMs !== undefined || - state.kind === 'stopped' || - (state.kind === 'creating' && - !deps.isCreateInFlight() && - binding === undefined && - !context.measurementStarted) - ) { - if (deliveriesInFlight.has(generation)) { - await defer(generation, entry.callback); - continue; - } - deliveriesInFlight.add(generation); - await defer(generation, entry.callback); - deps.waitUntil( - active.billing - .deliverSettlement(generation) - .catch(() => defer(generation, entry.callback)) - .finally(() => deliveriesInFlight.delete(generation)) - ); - } else if (!context.measurementStarted) { - await deps.schedule.schedule( - entry.callback, - Date.now() + DEFAULT_BILLING_HEARTBEAT_SECONDS * 1_000, - generation - ); - } else { - try { - await active.heartbeat.billingHeartbeatTick(generation); - } catch { - await defer(generation, entry.callback); - } - } - } - } - - async function defer(generation: string, callback: string): Promise { - await deps.schedule.deferRetry( - callback, - generation, - Date.now() + VERCEL_BILLING_DELIVERY_RETRY_MS - ); - } - - return { - ensureRuntime, - admitCreate, - admitShadow, - recordLifetime, - settleStoppedCreatedRef, - afterTransition, - prepareSettlement, - runAlarm, - defer, - }; -} diff --git a/services/cloud-agent-next/src/control-plane/session/failure-messages.ts b/services/cloud-agent-next/src/control-plane/session/failure-messages.ts index 53e035e6875..c8a1f33fbc0 100644 --- a/services/cloud-agent-next/src/control-plane/session/failure-messages.ts +++ b/services/cloud-agent-next/src/control-plane/session/failure-messages.ts @@ -29,6 +29,11 @@ const CONTROL_REASON_MESSAGES: Record = { prompt_failed: 'Prompt delivery failed', sandbox_stopped: 'The sandbox stopped while waiting for an answer', execution_limit: 'The turn exceeded its time limit', + byoc_credential_missing: 'Vercel compute credentials are no longer available', + byoc_vercel_not_ready: 'Vercel compute setup is not ready', + byoc_vercel_forbidden: 'Vercel rejected access to your compute account', + byoc_vercel_capacity: 'Vercel compute capacity is unavailable', + byoc_vercel_snapshot_missing: 'The Vercel runtime snapshot is missing. Run setup again.', }; export function isControlPlaneFailureReason(value: string): value is ControlPlaneFailureReason { diff --git a/services/cloud-agent-next/src/control-plane/session/registration.test.ts b/services/cloud-agent-next/src/control-plane/session/registration.test.ts index 7d4057bb9ef..f2dc27e57c1 100644 --- a/services/cloud-agent-next/src/control-plane/session/registration.test.ts +++ b/services/cloud-agent-next/src/control-plane/session/registration.test.ts @@ -167,7 +167,10 @@ describe('egress policy registration', () => { it.each([ { provider: 'cloudflare' }, - { provider: 'vercel' }, + { + provider: 'vercel', + byoc: { userId: 'owner-1', credentialId: '22222222-2222-4222-8222-222222222222' }, + }, { provider: 'cloudflare-containers', sandboxKind: 'shared' }, { provider: 'cloudflare-containers', sandboxKind: 'code-review' }, ])('restricts only allowlist mode to isolated containers: %j', placement => { diff --git a/services/cloud-agent-next/src/control-plane/session/sandbox-selection.ts b/services/cloud-agent-next/src/control-plane/session/sandbox-selection.ts index 07d8c9160b1..599c83496e9 100644 --- a/services/cloud-agent-next/src/control-plane/session/sandbox-selection.ts +++ b/services/cloud-agent-next/src/control-plane/session/sandbox-selection.ts @@ -1,3 +1,4 @@ +import { byocVercelOwnerSchema } from '../../byoc/vercel-owner.js'; import { z } from 'zod'; import { egressPolicySchema } from '@kilocode/worker-utils/egress-allowlist'; import { agentSandboxProviderSchema } from '../../types.js'; @@ -25,6 +26,7 @@ export const controlPlaneSandboxSelectionSchema = egressPolicySchema containment: credentialContainmentSchema.optional(), /** The stored kind of a placed (`sbx-`) key; absent for a legacy prefixed key. */ sandboxKind: sandboxKindSchema.optional(), + byoc: z.object({ credentialId: z.uuid() }).and(byocVercelOwnerSchema).optional(), }) .strict() .refine( @@ -33,6 +35,11 @@ export const controlPlaneSandboxSelectionSchema = egressPolicySchema (selection.provider === 'cloudflare-containers' && (selection.sandboxKind === undefined || selection.sandboxKind === 'isolated')), 'allowlist egress requires an isolated Cloudflare containers sandbox' + ) + .refine(input => input.byoc === undefined || input.provider === 'vercel', 'BYOC requires Vercel') + .refine( + input => input.provider !== 'vercel' || input.byoc !== undefined, + 'Vercel requires BYOC credentials' ); export type ControlPlaneSandboxSelection = z.infer; diff --git a/services/cloud-agent-next/src/control-plane/session/session-do.ts b/services/cloud-agent-next/src/control-plane/session/session-do.ts index cd1a9d2120e..ec7abd1ec5f 100644 --- a/services/cloud-agent-next/src/control-plane/session/session-do.ts +++ b/services/cloud-agent-next/src/control-plane/session/session-do.ts @@ -31,6 +31,8 @@ import { z } from 'zod'; import { resolveSecret } from '../../auth.js'; import type { CallbackTarget } from '../../callbacks/index.js'; import type { DurableAdmissionAck } from '../../execution/types.js'; +import { getSandboxProviderBinding } from '../../persistence/session-metadata.js'; +import { sameSandboxProviderBinding } from '../../sandbox-provider-binding.js'; import { logger } from '../../logger.js'; import type { WrapperPty } from '../../terminal/types.js'; import type { OperationResult } from '../../persistence/types.js'; @@ -1300,6 +1302,10 @@ export class SandboxSessionV2 extends DurableObject { (existing.allowSubagentSessions ?? false) === (metadata.allowSubagentSessions ?? false) && existing.parentCloudAgentSessionId === metadata.parentCloudAgentSessionId && sameRepository(existing.repository, metadata.repository) && + sameSandboxProviderBinding( + getSandboxProviderBinding(existing), + getSandboxProviderBinding(metadata) + ) && this.registration.sandboxId === metadata.workspace?.sandboxId && (existing.workspace?.egressMode ?? 'unrestricted') === (metadata.workspace?.egressMode ?? 'unrestricted') && diff --git a/services/cloud-agent-next/src/db/vercel-snapshot-schema.ts b/services/cloud-agent-next/src/db/vercel-snapshot-schema.ts new file mode 100644 index 00000000000..6ff22940475 --- /dev/null +++ b/services/cloud-agent-next/src/db/vercel-snapshot-schema.ts @@ -0,0 +1,33 @@ +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; + +export const vercelSnapshotBuilds = sqliteTable('vercel_snapshot_builds', { + organization_id: text('organization_id').primaryKey(), + owner_type: text('owner_type').notNull().default('org'), + credential_id: text('credential_id').notNull(), + build_generation: text('build_generation').notNull(), + runtime_build_id: text('runtime_build_id').notNull(), + step: text('step').notNull(), + retry_count: integer('retry_count').notNull().default(0), + next_attempt_at: integer('next_attempt_at'), + builder_name: text('builder_name').notNull(), + builder_operation_id: text('builder_operation_id').notNull(), + builder_create_requested: integer('builder_create_requested').notNull().default(0), + builder_session_id: text('builder_session_id'), + validator_name: text('validator_name').notNull(), + validator_operation_id: text('validator_operation_id').notNull(), + validator_create_requested: integer('validator_create_requested').notNull().default(0), + validator_session_id: text('validator_session_id'), + validator_wrapper_requested: integer('validator_wrapper_requested').notNull().default(0), + validator_wrapper_command_id: text('validator_wrapper_command_id'), + validator_control_id: text('validator_control_id'), + team_slug: text('team_slug'), + project_slug: text('project_slug'), + snapshot_baseline_json: text('snapshot_baseline_json'), + snapshot_source_session_id: text('snapshot_source_session_id'), + snapshot_id: text('snapshot_id'), + snapshot_candidate_id: text('snapshot_candidate_id'), + snapshot_requested: integer('snapshot_requested').notNull().default(0), + last_error: text('last_error'), + created_at: integer('created_at').notNull(), + updated_at: integer('updated_at').notNull(), +}); diff --git a/services/cloud-agent-next/src/index.ts b/services/cloud-agent-next/src/index.ts index 3e04a9c0bb5..8e89620dfce 100644 --- a/services/cloud-agent-next/src/index.ts +++ b/services/cloud-agent-next/src/index.ts @@ -15,6 +15,7 @@ export { ContainersOutbound } from './sandbox-containers/containers-outbound.js' export { SandboxControlV2 as SandboxControl } from './control-plane/sandbox/sandbox-do.js'; export { SandboxSessionV2 as SandboxSession } from './control-plane/session/session-do.js'; export { SandboxContainers } from './sandbox-containers/SandboxContainers.js'; +export { VercelSnapshotBuild } from './persistence/VercelSnapshotBuild.js'; export { StreamTicketNonceDO } from './persistence/StreamTicketNonceDO.js'; export { CloudAgentSession } from './persistence/CloudAgentSession.js'; export { UserKiloFacade } from './kilo-facade/user-kilo-facade.js'; diff --git a/services/cloud-agent-next/src/kilo/runtime-credential-proxy-routes.ts b/services/cloud-agent-next/src/kilo/runtime-credential-proxy-routes.ts index dfc448a7ca3..d60e1aa1eed 100644 --- a/services/cloud-agent-next/src/kilo/runtime-credential-proxy-routes.ts +++ b/services/cloud-agent-next/src/kilo/runtime-credential-proxy-routes.ts @@ -175,6 +175,13 @@ function providerTargetUrl(base: string, path: string, search: string): URL | nu provider.search = search; return provider; } + // Local stacks and CLI configuration use the API root as the provider base. + // Resolve its model routes to the same OpenRouter endpoint as the CLI. + if (provider.pathname.replace(/\/+$/, '').endsWith('/api')) { + provider.pathname = `${provider.pathname.replace(/\/+$/, '')}/openrouter${path}`; + provider.search = search; + return provider; + } return targetUrl(base, path, search); } diff --git a/services/cloud-agent-next/src/persistence/VercelSnapshotBuild.test.ts b/services/cloud-agent-next/src/persistence/VercelSnapshotBuild.test.ts new file mode 100644 index 00000000000..00e17f72898 --- /dev/null +++ b/services/cloud-agent-next/src/persistence/VercelSnapshotBuild.test.ts @@ -0,0 +1,2245 @@ +import { readFileSync } from 'node:fs'; +import { DatabaseSync } from 'node:sqlite'; + +import { eq } from 'drizzle-orm'; +import { drizzle } from 'drizzle-orm/durable-sqlite'; +import { migrate } from 'drizzle-orm/durable-sqlite/migrator'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { vercelSnapshotBuilds } from '../db/vercel-snapshot-schema.js'; +import { + hashSandboxCredential, + verifySandboxLaunchCredential, +} from '../sandbox-control/credential.js'; +import { encodeVercelProviderRef } from '../sandbox-control/vercel-provider.js'; + +const mocks = vi.hoisted(() => { + class CredentialMissingError extends Error {} + class CredentialResolverError extends Error {} + class SandboxRestError extends Error { + constructor( + readonly kind: string, + readonly operation: string, + readonly status?: number + ) { + super(`${kind}:${operation}`); + } + } + + const client = { + inspectTeam: vi.fn(), + inspectProject: vi.fn(), + createSandbox: vi.fn(), + inspectByName: vi.fn(), + executeCommand: vi.fn(), + writeFiles: vi.fn(), + listCommands: vi.fn(), + getCommand: vi.fn(), + stopSession: vi.fn(), + getSession: vi.fn(), + listSnapshots: vi.fn(), + inspectSnapshot: vi.fn(), + createSnapshot: vi.fn(), + deleteSnapshot: vi.fn(), + }; + + return { + CredentialMissingError, + CredentialResolverError, + SandboxRestError, + client, + control: { + initializeSnapshotValidator: vi.fn(), + confirmSnapshotValidatorStopped: vi.fn(), + getSnapshotValidatorStatus: vi.fn(), + }, + fetchCredential: vi.fn(), + resolveAccess: vi.fn(), + projectStatus: vi.fn(), + getArtifacts: vi.fn(), + getIdentity: vi.fn(), + }; +}); + +vi.mock('cloudflare:workers', () => ({ + DurableObject: class DurableObject { + constructor( + readonly ctx: unknown, + readonly env: unknown + ) {} + }, +})); + +vi.mock('../../drizzle/vercel-snapshot/migrations', async () => { + const { readFileSync } = await import('node:fs'); + const journal = JSON.parse( + readFileSync( + new URL('../../drizzle/vercel-snapshot/meta/_journal.json', import.meta.url).pathname, + 'utf8' + ) + ); + + return { + default: { + journal, + migrations: { + m0000: readFileSync( + new URL('../../drizzle/vercel-snapshot/0000_vercel_snapshot_build.sql', import.meta.url) + .pathname, + 'utf8' + ), + }, + }, + }; +}); + +vi.mock('../byoc/vercel-credential-resolver.js', () => ({ + ByocCredentialMissingError: mocks.CredentialMissingError, + ByocCredentialResolverError: mocks.CredentialResolverError, + fetchByocVercelCredential: mocks.fetchCredential, + resolveByocVercelAccessConfig: mocks.resolveAccess, + projectByocVercelStatus: mocks.projectStatus, +})); + +vi.mock('../byoc/vercel-runtime-artifacts.js', () => ({ + getVercelRuntimeArtifacts: mocks.getArtifacts, +})); + +vi.mock('../byoc/vercel-runtime-identity.js', () => ({ + getVercelRuntimeIdentity: mocks.getIdentity, + VERCEL_RUNTIME_BUN_VERSION: '1.3.14', + VERCEL_RUNTIME_KILO_CLI_VERSION: '7.4.20', +})); + +vi.mock('../agent-sandbox/vercel/vercel-sandbox-rest-client.js', () => ({ + VERCEL_CLOUD_AGENT_CREATE_OPERATION_TAG: 'kilo-create-operation', + VERCEL_CLOUD_AGENT_RESOURCE_TAG: 'kilo-managed-by', + VERCEL_CLOUD_AGENT_RESOURCE_TAG_VALUE: 'cloud-agent-session', + VERCEL_CLOUD_AGENT_RUNTIME_BUILD_TAG: 'kilo-runtime-build', + VercelSandboxRestError: mocks.SandboxRestError, + VercelSandboxRestClient: class VercelSandboxRestClient { + constructor() { + Object.assign(this, mocks.client); + } + }, +})); + +vi.mock('../utils/do-retry.js', () => ({ + withDORetry: async ( + getStub: () => unknown, + operation: (stub: unknown) => Promise + ): Promise => operation(getStub()), +})); + +vi.mock('../logger.js', () => ({ + logger: { + withFields: () => ({ + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + }), + }, +})); + +const { VercelSnapshotBuild } = await import('./VercelSnapshotBuild.js'); +const { + ownerCredentialFetchInput, + ownerFromInput, + ownerFromRow, + ownerToProjection, + ownerToRowKey, +} = await import('../byoc/vercel-snapshot-owner.js'); + +const ORGANIZATION_ID = '11111111-1111-4111-8111-111111111111'; +const CREDENTIAL_ID = '22222222-2222-4222-8222-222222222222'; +const FIRST_GENERATION = '33333333-3333-4333-8333-333333333333'; +const SECOND_GENERATION = '44444444-4444-4444-8444-444444444444'; +const THIRD_GENERATION = '55555555-5555-4555-8555-555555555555'; +const PERSONAL_USER_ID = 'oauth/vercel-user'; + +type BuildRow = typeof vercelSnapshotBuilds.$inferSelect; +type BuildChanges = Partial; +type ManagedSandboxKind = 'builder' | 'validator'; +type BuildOwnerInput = { organizationId: string } | { userId: string }; + +type StorageFixture = ReturnType; +type BuildFixture = { + database: DatabaseSync; + storage: StorageFixture; + build: InstanceType; +}; + +function makeCredential( + buildGeneration = FIRST_GENERATION, + owner: BuildOwnerInput = { organizationId: ORGANIZATION_ID } +) { + return { + ...owner, + credentialId: CREDENTIAL_ID, + buildGeneration, + runtimeBuildId: `runtime-${buildGeneration}`, + }; +} + +function startInput( + buildGeneration = FIRST_GENERATION, + owner: BuildOwnerInput = { organizationId: ORGANIZATION_ID } +) { + return { + ...owner, + credentialId: CREDENTIAL_ID, + buildGeneration, + }; +} + +function createStorage(database: DatabaseSync) { + let alarm: number | null = null; + + return { + sql: { + exec(query: string, ...parameters: unknown[]) { + const statement = database.prepare(query); + const columns = statement.columns().map(column => column.name); + const rows = statement.all(...(parameters as never[])); + const iterator = rows[Symbol.iterator](); + + return { + next: () => iterator.next(), + toArray: () => rows, + raw: () => ({ + toArray: () => rows.map(row => columns.map(column => row[column])), + }), + [Symbol.iterator]: () => rows[Symbol.iterator](), + }; + }, + }, + transactionSync(action: () => T): T { + database.exec('BEGIN'); + try { + const result = action(); + database.exec('COMMIT'); + return result; + } catch (error) { + database.exec('ROLLBACK'); + throw error; + } + }, + setAlarm: vi.fn(async (timestamp: number | Date) => { + alarm = timestamp instanceof Date ? timestamp.getTime() : timestamp; + }), + deleteAlarm: vi.fn(async () => { + alarm = null; + }), + getAlarm: vi.fn(async () => alarm), + }; +} + +function createBuild( + storage: StorageFixture, + ownerKey = ORGANIZATION_ID +): InstanceType { + const ctx = { + id: { name: ownerKey }, + storage, + blockConcurrencyWhile: vi.fn((action: () => Promise) => action()), + }; + const env = { + WORKER_URL: 'https://worker.invalid', + NEXTAUTH_SECRET: 'snapshot-test-signing-secret', + SANDBOX_CONTROL: { getByName: () => mocks.control }, + }; + return new VercelSnapshotBuild(ctx as never, env as never); +} + +function createFixture(): BuildFixture { + const database = new DatabaseSync(':memory:'); + const storage = createStorage(database); + return { database, storage, build: createBuild(storage) }; +} + +function storedRow(fixture: BuildFixture): BuildRow | undefined { + return storedRowAt(fixture, ORGANIZATION_ID); +} + +function storedRowAt(fixture: BuildFixture, ownerKey: string): BuildRow | undefined { + return drizzle(fixture.storage as never) + .select() + .from(vercelSnapshotBuilds) + .where(eq(vercelSnapshotBuilds.organization_id, ownerKey)) + .get(); +} + +function updateRow(fixture: BuildFixture, changes: BuildChanges, ownerKey = ORGANIZATION_ID): void { + drizzle(fixture.storage as never) + .update(vercelSnapshotBuilds) + .set(changes) + .where(eq(vercelSnapshotBuilds.organization_id, ownerKey)) + .run(); +} + +async function runNextAlarm(fixture: BuildFixture): Promise { + const state = storedRow(fixture); + expect(state?.next_attempt_at).toEqual(expect.any(Number)); + expect(await fixture.storage.getAlarm()).not.toBeNull(); + updateRow(fixture, { next_attempt_at: Date.now() - 1 }); + await fixture.build.alarm(); +} + +function prepareCreation(fixture: BuildFixture, kind: ManagedSandboxKind, requested = 1): BuildRow { + if (kind === 'builder') { + updateRow(fixture, { step: 'create_builder', builder_create_requested: requested }); + } else { + updateRow(fixture, { + step: 'create_validator', + builder_session_id: 'builder-snapshotted', + snapshot_id: 'snapshot-source', + validator_create_requested: requested, + }); + } + + const state = storedRow(fixture); + if (!state) throw new Error('Expected pending managed sandbox creation'); + return state; +} + +function createdSandbox(state: BuildRow, kind: ManagedSandboxKind, sessionId: string) { + const sandboxName = kind === 'builder' ? state.builder_name : state.validator_name; + const operation = kind === 'builder' ? state.builder_operation_id : state.validator_operation_id; + + return { + sandbox: { + name: sandboxName, + currentSessionId: sessionId, + status: 'running', + persistent: false, + createdAt: 0, + updatedAt: 0, + tags: { + 'kilo-managed-by': 'cloud-agent-session', + 'kilo-create-operation': operation, + 'kilo-runtime-build': state.runtime_build_id, + }, + }, + session: { + id: sessionId, + sourceSandboxName: sandboxName, + projectId: 'project-test', + ...(kind === 'validator' ? { sourceSnapshotId: state.snapshot_id } : {}), + runtime: 'node24', + status: 'running', + memory: 2048, + vcpus: 2, + region: 'iad1', + timeout: 600_000, + requestedAt: 0, + cwd: '/', + createdAt: 0, + updatedAt: 0, + }, + routes: [], + runtime: { sandboxName, sessionId }, + }; +} + +function deferred() { + let resolve: (value: T) => void = () => undefined; + let reject: (error: unknown) => void = () => undefined; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + + return { promise, resolve, reject }; +} + +beforeEach(() => { + vi.resetAllMocks(); + mocks.fetchCredential.mockResolvedValue(makeCredential()); + mocks.resolveAccess.mockResolvedValue({ + accessToken: 'test-vercel-credential', + teamId: 'team-test', + projectId: 'project-test', + }); + mocks.projectStatus.mockResolvedValue(true); + mocks.control.initializeSnapshotValidator.mockResolvedValue(undefined); + mocks.control.confirmSnapshotValidatorStopped.mockResolvedValue(undefined); + mocks.control.getSnapshotValidatorStatus.mockResolvedValue({ + physical: 'running', + connection: 'ready', + }); + mocks.client.getSession.mockImplementation(async () => ({ + session: { status: 'running', requestedAt: Date.now(), timeout: 600_000 }, + })); + mocks.client.inspectTeam.mockResolvedValue({ id: 'team-test', slug: 'team-test' }); + mocks.client.inspectProject.mockResolvedValue({ id: 'project-test', name: 'project-test' }); + mocks.client.inspectByName.mockResolvedValue(null); + mocks.client.listSnapshots.mockResolvedValue([]); + mocks.client.stopSession.mockResolvedValue({ status: 'stopped' }); + mocks.client.deleteSnapshot.mockResolvedValue(undefined); + mocks.client.writeFiles.mockResolvedValue(undefined); + mocks.client.executeCommand.mockImplementation(async (_sessionId, input) => + input.wait === false + ? { id: 'command-test', exitCode: null } + : { + command: { id: 'command-test', exitCode: null }, + finished: { id: 'command-test', exitCode: 0 }, + } + ); + mocks.getIdentity.mockResolvedValue({ + wrapperVersion: '2.4.0', + releasedAt: '2026-09-08', + digest: 'digest-test', + }); + mocks.getArtifacts.mockResolvedValue([ + { + path: 'usr/local/bin/kilocode-control-plane-wrapper.js', + bytes: new TextEncoder().encode('control-wrapper'), + sha256: 'b'.repeat(64), + }, + { + path: 'usr/local/bin/kilocode-control-plane-supervisor.sh', + bytes: new TextEncoder().encode('supervisor'), + sha256: 'c'.repeat(64), + }, + ]); +}); + +describe('VercelSnapshotBuild persistence isolation', () => { + it('applies snapshot-only migrations without adding snapshot tables to session migrations', async () => { + const snapshot = createFixture(); + const snapshotTables = snapshot.database + .prepare("SELECT name FROM sqlite_master WHERE type = 'table'") + .all() + .map(row => row.name); + + expect(snapshotTables).toContain('vercel_snapshot_builds'); + expect(snapshotTables).not.toContain('events'); + expect(snapshotTables).not.toContain('command_queue'); + expect(snapshotTables).not.toContain('execution_leases'); + + const sessionDatabase = new DatabaseSync(':memory:'); + const sessionStorage = createStorage(sessionDatabase); + const journal = JSON.parse( + readFileSync( + new URL('../control-plane/session/drizzle/meta/_journal.json', import.meta.url).pathname, + 'utf8' + ) + ) as { + entries: Array<{ idx: number; when: number; tag: string; breakpoints: boolean }>; + }; + const migrations = Object.fromEntries( + journal.entries.map(entry => [ + `m${String(entry.idx).padStart(4, '0')}`, + readFileSync( + new URL(`../control-plane/session/drizzle/${entry.tag}.sql`, import.meta.url).pathname, + 'utf8' + ), + ]) + ); + + await migrate(drizzle(sessionStorage as never), { journal, migrations }); + + const sessionTables = sessionDatabase + .prepare("SELECT name FROM sqlite_master WHERE type = 'table'") + .all() + .map(row => row.name); + expect(sessionTables).toEqual(expect.arrayContaining(['events', 'control_plane_messages'])); + expect(sessionTables).not.toContain('vercel_snapshot_builds'); + }); + + it('maps personal owners to prefixed rows and owner-shaped resolver inputs', () => { + const owner = { type: 'user' as const, userId: PERSONAL_USER_ID }; + + expect(ownerToRowKey(owner)).toBe(`user:${PERSONAL_USER_ID}`); + expect( + ownerFromRow({ organization_id: `user:${PERSONAL_USER_ID}`, owner_type: 'user' }) + ).toEqual(owner); + expect(ownerFromRow({ organization_id: ORGANIZATION_ID, owner_type: 'org' })).toEqual({ + type: 'org', + organizationId: ORGANIZATION_ID, + }); + expect(ownerToProjection(owner)).toEqual({ userId: PERSONAL_USER_ID }); + expect(ownerCredentialFetchInput(owner, CREDENTIAL_ID)).toEqual({ + userId: PERSONAL_USER_ID, + credentialId: CREDENTIAL_ID, + }); + expect(ownerFromInput(startInput(FIRST_GENERATION, { userId: PERSONAL_USER_ID }))).toEqual( + owner + ); + }); + + it('persists and cleans personal snapshot builds through the user-keyed row', async () => { + const database = new DatabaseSync(':memory:'); + const storage = createStorage(database); + const build = createBuild(storage, `user:${PERSONAL_USER_ID}`); + const input = startInput(FIRST_GENERATION, { userId: PERSONAL_USER_ID }); + mocks.fetchCredential.mockResolvedValueOnce( + makeCredential(FIRST_GENERATION, { userId: PERSONAL_USER_ID }) + ); + + await build.start(input); + + expect(storedRowAt({ database, storage, build }, `user:${PERSONAL_USER_ID}`)).toMatchObject({ + organization_id: `user:${PERSONAL_USER_ID}`, + owner_type: 'user', + credential_id: CREDENTIAL_ID, + }); + expect(mocks.fetchCredential).toHaveBeenCalledWith(expect.anything(), { + userId: PERSONAL_USER_ID, + credentialId: CREDENTIAL_ID, + }); + + await build.cleanup(input); + + expect(storedRowAt({ database, storage, build }, `user:${PERSONAL_USER_ID}`)).toBeUndefined(); + }); + + it('uses persisted personal ownership for alarm projection', async () => { + const database = new DatabaseSync(':memory:'); + const storage = createStorage(database); + const build = createBuild(storage, `user:${PERSONAL_USER_ID}`); + const input = startInput(FIRST_GENERATION, { userId: PERSONAL_USER_ID }); + mocks.fetchCredential.mockResolvedValueOnce( + makeCredential(FIRST_GENERATION, { userId: PERSONAL_USER_ID }) + ); + + await build.start(input); + updateRow( + { database, storage, build }, + { next_attempt_at: Date.now() - 1 }, + `user:${PERSONAL_USER_ID}` + ); + await build.alarm(); + + expect(mocks.projectStatus).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + userId: PERSONAL_USER_ID, + credentialId: CREDENTIAL_ID, + setupStatus: 'building', + }) + ); + }); + + it('rejects a persisted owner discriminator that disagrees with the row key before lookup', async () => { + const database = new DatabaseSync(':memory:'); + const storage = createStorage(database); + const build = createBuild(storage, `user:${PERSONAL_USER_ID}`); + await build.start(startInput(FIRST_GENERATION, { userId: PERSONAL_USER_ID })); + updateRow( + { database, storage, build }, + { owner_type: 'org', next_attempt_at: Date.now() - 1 }, + `user:${PERSONAL_USER_ID}` + ); + mocks.fetchCredential.mockClear(); + mocks.control.initializeSnapshotValidator.mockClear(); + + await expect(build.alarm()).rejects.toThrow('snapshot_owner_row_key_mismatch'); + + expect(mocks.fetchCredential).not.toHaveBeenCalled(); + expect(mocks.control.initializeSnapshotValidator).not.toHaveBeenCalled(); + }); +}); + +describe('VercelSnapshotBuild token scope', () => { + it('does not inspect team metadata for a project-scoped credential', async () => { + const fixture = createFixture(); + mocks.resolveAccess.mockResolvedValueOnce({ + accessToken: 'test-vercel-credential', + teamId: 'team-test', + scope: 'project', + projectId: 'project-test', + }); + + await fixture.build.start(startInput()); + await fixture.build.alarm(); + + expect(mocks.client.inspectTeam).not.toHaveBeenCalled(); + expect(mocks.client.inspectProject).toHaveBeenCalledOnce(); + }); +}); + +describe('VercelSnapshotBuild ownership fencing', () => { + it('re-arms a persisted build when the first start could not set its alarm', async () => { + const fixture = createFixture(); + fixture.storage.setAlarm.mockRejectedValueOnce(new Error('alarm unavailable')); + await expect(fixture.build.start(startInput())).rejects.toThrow('alarm unavailable'); + expect(await fixture.storage.getAlarm()).toBeNull(); + + await fixture.build.start(startInput()); + await runNextAlarm(fixture); + + expect(storedRow(fixture)?.team_slug).toBe('team-test'); + }); + + it('retries failed generation handover cleanup without a new start request', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { builder_session_id: 'builder-old' }); + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + mocks.client.stopSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'stop-session', 503) + ); + await expect(fixture.build.start(startInput(SECOND_GENERATION))).rejects.toMatchObject({ + status: 503, + }); + expect(storedRow(fixture)).toMatchObject({ step: 'cleanup' }); + expect(storedRow(fixture)?.next_attempt_at).not.toBeNull(); + await runNextAlarm(fixture); + expect(storedRow(fixture)).toBeUndefined(); + + await fixture.build.start(startInput(SECOND_GENERATION)); + await runNextAlarm(fixture); + + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + runtime_build_id: `runtime-${SECOND_GENERATION}`, + builder_session_id: null, + team_slug: 'team-test', + }); + }); + + it('deletes a superseded snapshot before advancing a replacement with the same credential', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { snapshot_id: 'snapshot-superseded' }); + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + + await fixture.build.start(startInput(SECOND_GENERATION)); + + expect(mocks.client.deleteSnapshot).toHaveBeenCalledWith('snapshot-superseded'); + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + snapshot_id: null, + }); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + }); + + it('retains the superseded builder until fenced replacement cleanup confirms it stopped', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { builder_session_id: 'builder-old' }); + const previous = storedRow(fixture); + if (!previous) throw new Error('Expected previous build state'); + + const stopping = deferred<{ status: string }>(); + mocks.client.stopSession.mockImplementationOnce(() => stopping.promise); + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + const replacement = fixture.build.start(startInput(SECOND_GENERATION)); + await vi.waitFor(() => expect(mocks.client.stopSession).toHaveBeenCalledOnce()); + + expect(mocks.client.stopSession).toHaveBeenCalledWith('builder-old', previous.builder_name); + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + builder_session_id: 'builder-old', + builder_name: previous.builder_name, + next_attempt_at: null, + }); + + stopping.resolve({ status: 'stopped' }); + await replacement; + + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + builder_session_id: null, + step: 'validating_access', + }); + }); + + it('preserves superseded sessions and propagates replacement cleanup failures', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { builder_session_id: 'builder-old' }); + const previous = storedRow(fixture); + if (!previous) throw new Error('Expected previous build state'); + + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + mocks.client.stopSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'stop-session', 503) + ); + + await expect(fixture.build.start(startInput(SECOND_GENERATION))).rejects.toMatchObject({ + operation: 'stop-session', + status: 503, + }); + + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + builder_session_id: 'builder-old', + builder_name: previous.builder_name, + step: 'cleanup', + }); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + + await runNextAlarm(fixture); + + expect(mocks.client.stopSession).toHaveBeenCalledTimes(2); + expect(storedRow(fixture)).toBeUndefined(); + }); + + it('does not allow a delayed replacement start to overwrite newer ownership', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + + const staleCredential = deferred>(); + mocks.fetchCredential.mockImplementationOnce(() => staleCredential.promise); + const staleStart = fixture.build.start(startInput(SECOND_GENERATION)); + + mocks.fetchCredential.mockResolvedValueOnce(makeCredential(THIRD_GENERATION)); + await fixture.build.start(startInput(THIRD_GENERATION)); + + staleCredential.resolve(makeCredential(SECOND_GENERATION)); + await staleStart; + + expect(storedRow(fixture)?.build_generation).toBe(THIRD_GENERATION); + expect(fixture.storage.setAlarm).toHaveBeenCalledTimes(2); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + }); + + it('prevents a stale alarm from projecting, overwriting, or rearming newer ownership', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + + const inspection = deferred<{ id: string; slug: string }>(); + mocks.client.inspectTeam.mockImplementationOnce(() => inspection.promise); + const staleAlarm = fixture.build.alarm(); + await vi.waitFor(() => expect(mocks.client.inspectTeam).toHaveBeenCalledOnce()); + + mocks.fetchCredential.mockResolvedValueOnce(makeCredential(SECOND_GENERATION)); + await fixture.build.start(startInput(SECOND_GENERATION)); + const armedByReplacement = fixture.storage.setAlarm.mock.calls.length; + + inspection.resolve({ id: 'team-test', slug: 'stale-team' }); + await staleAlarm; + + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + step: 'validating_access', + team_slug: null, + }); + expect(mocks.projectStatus).not.toHaveBeenCalled(); + expect(fixture.storage.setAlarm).toHaveBeenCalledTimes(armedByReplacement); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + }); + + it('does not let a stale missing-credential alarm erase or cancel a replacement', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + + const lookup = deferred>(); + mocks.fetchCredential.mockImplementationOnce(() => lookup.promise); + const staleAlarm = fixture.build.alarm(); + + mocks.fetchCredential.mockResolvedValueOnce(makeCredential(SECOND_GENERATION)); + await fixture.build.start(startInput(SECOND_GENERATION)); + lookup.reject(new mocks.CredentialMissingError('removed')); + await staleAlarm; + + expect(storedRow(fixture)?.build_generation).toBe(SECOND_GENERATION); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + }); + + it('removes a detached snapshot without allowing its stale alarm to mutate a newer generation', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'snapshot_builder', + snapshot_source_session_id: 'builder-session', + snapshot_baseline_json: '[]', + snapshot_requested: 1, + }); + + const pendingSnapshot = deferred<{ + id: string; + sourceSessionId: string; + status: string; + }>(); + mocks.client.createSnapshot.mockImplementationOnce(() => pendingSnapshot.promise); + const staleAlarm = fixture.build.alarm(); + await vi.waitFor(() => expect(mocks.client.createSnapshot).toHaveBeenCalledOnce()); + + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + await fixture.build.start(startInput(SECOND_GENERATION)); + pendingSnapshot.resolve({ + id: 'snapshot-detached', + sourceSessionId: 'builder-session', + status: 'created', + }); + await staleAlarm; + + expect(mocks.client.deleteSnapshot).toHaveBeenCalledWith('snapshot-detached'); + expect(storedRow(fixture)?.build_generation).toBe(SECOND_GENERATION); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + }); +}); + +describe('VercelSnapshotBuild create-intent cleanup', () => { + it.each(['builder', 'validator'] as const)( + 'cleans up a definitively rejected %s create without retaining its intent', + async kind => { + for (const status of [400, 401, 403]) { + const fixture = createFixture(); + await fixture.build.start(startInput()); + prepareCreation(fixture, kind); + mocks.client.createSandbox.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'create', status) + ); + + await runNextAlarm(fixture); + expect(storedRow(fixture)).toMatchObject({ + step: 'project_failure', + ...(kind === 'builder' + ? { builder_create_requested: 0 } + : { validator_create_requested: 0 }), + }); + await runNextAlarm(fixture); + expect(storedRow(fixture)).toBeUndefined(); + } + } + ); + + it.each(['builder', 'validator'] as const)( + 'clears a rate-limited %s create intent so retry and cleanup can recover', + async kind => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + prepareCreation(fixture, kind); + mocks.client.createSandbox.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'create', 429) + ); + await runNextAlarm(fixture); + expect(storedRow(fixture)).toMatchObject({ + retry_count: 1, + ...(kind === 'builder' + ? { builder_create_requested: 0 } + : { validator_create_requested: 0 }), + }); + await fixture.build.cleanup(startInput()); + expect(storedRow(fixture)).toBeUndefined(); + } + ); + + it.each(['builder', 'validator'] as const)( + 'retains the %s create intent after an ambiguous server failure', + async kind => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + prepareCreation(fixture, kind); + mocks.client.createSandbox.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'create', 503) + ); + + await runNextAlarm(fixture); + expect(storedRow(fixture)).toMatchObject( + kind === 'builder' ? { builder_create_requested: 2 } : { validator_create_requested: 2 } + ); + await expect(fixture.build.cleanup(startInput())).rejects.toMatchObject({ + operation: 'inspect', + }); + } + ); + + it.each(['builder', 'validator'] as const)( + 'allows an absent prepared %s intent to be canceled before provider creation', + async kind => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + const state = prepareCreation(fixture, kind); + + await fixture.build.cleanup(startInput()); + + expect(mocks.client.inspectByName).toHaveBeenCalledWith( + expect.objectContaining({ + name: kind === 'builder' ? state.builder_name : state.validator_name, + operationId: + kind === 'builder' ? state.builder_operation_id : state.validator_operation_id, + runtimeBuildId: state.runtime_build_id, + source: + kind === 'builder' + ? { type: 'runtime' } + : { type: 'snapshot', snapshotId: 'snapshot-source' }, + }) + ); + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + expect(storedRow(fixture)).toBeUndefined(); + } + ); + + it.each(['builder', 'validator'] as const)( + 'retains an ambiguous dispatched %s intent when named inspection returns not found', + async kind => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + prepareCreation(fixture, kind, 2); + + await expect(fixture.build.cleanup(startInput())).rejects.toMatchObject({ + kind: 'request_failed', + operation: 'inspect', + }); + + expect(storedRow(fixture)).toMatchObject( + kind === 'builder' ? { builder_create_requested: 2 } : { validator_create_requested: 2 } + ); + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + expect(mocks.client.deleteSnapshot).not.toHaveBeenCalled(); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + } + ); + + it.each(['builder', 'validator'] as const)( + 'reconciles and stops only the exactly tagged managed %s session', + async kind => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + const state = prepareCreation(fixture, kind, 2); + const sessionId = `${kind}-recovered`; + mocks.client.inspectByName.mockResolvedValueOnce(createdSandbox(state, kind, sessionId)); + + await fixture.build.cleanup(startInput()); + + expect(mocks.client.stopSession).toHaveBeenCalledWith( + sessionId, + kind === 'builder' ? state.builder_name : state.validator_name + ); + expect(storedRow(fixture)).toBeUndefined(); + } + ); + + it.each(['builder', 'validator'] as const)( + 'retains a %s intent when provider identity does not correlate', + async kind => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + prepareCreation(fixture, kind, 2); + mocks.client.inspectByName.mockRejectedValueOnce( + new mocks.SandboxRestError('correlation_mismatch', 'inspect') + ); + + await expect(fixture.build.cleanup(startInput())).rejects.toMatchObject({ + kind: 'correlation_mismatch', + operation: 'inspect', + }); + + expect(storedRow(fixture)).toBeDefined(); + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + } + ); + + it.each(['builder', 'validator'] as const)( + 'preserves a %s creation intent when provider reconciliation fails', + async kind => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + prepareCreation(fixture, kind, 2); + mocks.client.inspectByName.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'inspect', 503) + ); + + await expect(fixture.build.cleanup(startInput())).rejects.toMatchObject({ + operation: 'inspect', + status: 503, + }); + + expect(storedRow(fixture)).toMatchObject( + kind === 'builder' ? { builder_create_requested: 2 } : { validator_create_requested: 2 } + ); + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + } + ); + + it.each(['builder', 'validator'] as const)( + 'reconciles the previous %s intent during a fenced generation handover', + async kind => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + const previous = prepareCreation(fixture, kind, 2); + const sessionId = `${kind}-superseded`; + mocks.client.inspectByName.mockResolvedValueOnce(createdSandbox(previous, kind, sessionId)); + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + + await fixture.build.start(startInput(SECOND_GENERATION)); + + expect(mocks.client.inspectByName).toHaveBeenCalledWith( + expect.objectContaining({ + name: kind === 'builder' ? previous.builder_name : previous.validator_name, + operationId: + kind === 'builder' ? previous.builder_operation_id : previous.validator_operation_id, + runtimeBuildId: previous.runtime_build_id, + }) + ); + expect(mocks.client.stopSession).toHaveBeenCalledWith( + sessionId, + kind === 'builder' ? previous.builder_name : previous.validator_name + ); + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + builder_create_requested: 0, + validator_create_requested: 0, + builder_session_id: null, + validator_session_id: null, + }); + } + ); + + it.each(['builder', 'validator'] as const)( + 'stops a late %s create result after replacement and retains its exact session for retry', + async kind => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + const previous = prepareCreation(fixture, kind); + const sessionId = `${kind}-late`; + const creation = deferred>(); + mocks.client.createSandbox.mockImplementationOnce(() => creation.promise); + + const staleAlarm = fixture.build.alarm(); + await vi.waitFor(() => expect(mocks.client.createSandbox).toHaveBeenCalledOnce()); + expect(storedRow(fixture)).toMatchObject( + kind === 'builder' ? { builder_create_requested: 2 } : { validator_create_requested: 2 } + ); + + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + await expect(fixture.build.start(startInput(SECOND_GENERATION))).rejects.toMatchObject({ + operation: 'inspect', + }); + expect(storedRow(fixture)?.build_generation).toBe(SECOND_GENERATION); + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + + creation.resolve(createdSandbox(previous, kind, sessionId)); + await staleAlarm; + + expect(mocks.client.stopSession).toHaveBeenCalledWith( + sessionId, + kind === 'builder' ? previous.builder_name : previous.validator_name + ); + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + ...(kind === 'builder' + ? { builder_session_id: sessionId } + : { validator_session_id: sessionId }), + }); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + + await fixture.build.cleanup(startInput(SECOND_GENERATION)); + + expect(storedRow(fixture)).toBeUndefined(); + } + ); + + it.each(['builder', 'validator'] as const)( + 'idempotently stops a late %s create result after its owner was canceled', + async kind => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + const previous = prepareCreation(fixture, kind); + const sessionId = `${kind}-canceled`; + const envelope = createdSandbox(previous, kind, sessionId); + const creation = deferred>(); + mocks.client.createSandbox.mockImplementationOnce(() => creation.promise); + + const staleAlarm = fixture.build.alarm(); + await vi.waitFor(() => expect(mocks.client.createSandbox).toHaveBeenCalledOnce()); + mocks.client.inspectByName.mockResolvedValueOnce(envelope); + + await fixture.build.cleanup(startInput()); + + expect(storedRow(fixture)).toBeUndefined(); + mocks.client.stopSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'stop-session', kind === 'builder' ? 404 : 410) + ); + creation.resolve(envelope); + await staleAlarm; + + expect(mocks.client.stopSession.mock.calls).toEqual([ + [sessionId, kind === 'builder' ? previous.builder_name : previous.validator_name], + [sessionId, kind === 'builder' ? previous.builder_name : previous.validator_name], + ]); + expect(fixture.storage.deleteAlarm).toHaveBeenCalledOnce(); + } + ); + + it.each(['builder', 'validator'] as const)( + 'retains a late %s session when detached provider stop fails', + async kind => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + const previous = prepareCreation(fixture, kind); + const sessionId = `${kind}-retained`; + const creation = deferred>(); + mocks.client.createSandbox.mockImplementationOnce(() => creation.promise); + + const staleAlarm = fixture.build.alarm(); + await vi.waitFor(() => expect(mocks.client.createSandbox).toHaveBeenCalledOnce()); + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + await expect(fixture.build.start(startInput(SECOND_GENERATION))).rejects.toMatchObject({ + operation: 'inspect', + }); + mocks.client.stopSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'stop-session', 503) + ); + + creation.resolve(createdSandbox(previous, kind, sessionId)); + await staleAlarm; + + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + ...(kind === 'builder' + ? { builder_session_id: sessionId } + : { validator_session_id: sessionId }), + }); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + + await fixture.build.cleanup(startInput(SECOND_GENERATION)); + + expect(storedRow(fixture)).toBeUndefined(); + } + ); + + it('never stops a detached sandbox whose managed generation tags do not match', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + const previous = prepareCreation(fixture, 'builder'); + const creation = deferred>(); + mocks.client.createSandbox.mockImplementationOnce(() => creation.promise); + + const staleAlarm = fixture.build.alarm(); + await vi.waitFor(() => expect(mocks.client.createSandbox).toHaveBeenCalledOnce()); + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + await expect(fixture.build.start(startInput(SECOND_GENERATION))).rejects.toMatchObject({ + operation: 'inspect', + }); + + const untrusted = createdSandbox(previous, 'builder', 'customer-session'); + untrusted.sandbox.tags['kilo-create-operation'] = 'customer-owned'; + creation.resolve(untrusted); + await staleAlarm; + + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + builder_create_requested: 2, + builder_session_id: null, + }); + }); + + it('never stops a session already attributed to a newer generation', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + const previous = prepareCreation(fixture, 'builder'); + const sessionId = 'newer-generation-session'; + const creation = deferred>(); + mocks.client.createSandbox.mockImplementationOnce(() => creation.promise); + + const staleAlarm = fixture.build.alarm(); + await vi.waitFor(() => expect(mocks.client.createSandbox).toHaveBeenCalledOnce()); + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + await expect(fixture.build.start(startInput(SECOND_GENERATION))).rejects.toMatchObject({ + operation: 'inspect', + }); + updateRow(fixture, { + runtime_build_id: `runtime-${SECOND_GENERATION}`, + builder_name: 'ses-newer-generation', + builder_operation_id: 'byoc-builder-newer-generation', + builder_session_id: sessionId, + builder_create_requested: 1, + }); + + creation.resolve(createdSandbox(previous, 'builder', sessionId)); + await staleAlarm; + + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + builder_session_id: sessionId, + }); + }); + + it('does not reuse removed credentials for detached sandbox cleanup', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + const previous = prepareCreation(fixture, 'builder'); + const envelope = createdSandbox(previous, 'builder', 'builder-canceled'); + const creation = deferred>(); + mocks.client.createSandbox.mockImplementationOnce(() => creation.promise); + + const staleAlarm = fixture.build.alarm(); + await vi.waitFor(() => expect(mocks.client.createSandbox).toHaveBeenCalledOnce()); + mocks.client.inspectByName.mockResolvedValueOnce(envelope); + await fixture.build.cleanup(startInput()); + mocks.fetchCredential.mockRejectedValueOnce(new mocks.CredentialMissingError('removed')); + + creation.resolve(envelope); + await staleAlarm; + + expect(mocks.client.stopSession).toHaveBeenCalledOnce(); + expect(storedRow(fixture)).toBeUndefined(); + }); +}); + +describe('VercelSnapshotBuild cleanup', () => { + it('stops the exact builder when cancellation occurs before a snapshot exists', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'install_system_dependencies', + builder_session_id: 'builder-canceled', + }); + const state = storedRow(fixture); + if (!state) throw new Error('Expected active builder'); + + await fixture.build.cleanup(startInput()); + + expect(mocks.client.stopSession).toHaveBeenCalledOnce(); + expect(mocks.client.stopSession).toHaveBeenCalledWith('builder-canceled', state.builder_name); + expect(mocks.client.deleteSnapshot).not.toHaveBeenCalled(); + expect(storedRow(fixture)).toBeUndefined(); + expect(fixture.storage.deleteAlarm).toHaveBeenCalledOnce(); + }); + + it('stops each known validator and unsnapshotted builder using its exact persisted name', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + builder_session_id: 'builder-canceled', + validator_session_id: 'validator-canceled', + }); + const state = storedRow(fixture); + if (!state) throw new Error('Expected active setup sessions'); + + await fixture.build.cleanup(startInput()); + + expect(mocks.client.stopSession.mock.calls).toEqual([ + ['validator-canceled', state.validator_name], + ['builder-canceled', state.builder_name], + ]); + expect(storedRow(fixture)).toBeUndefined(); + }); + + it('stops the validator but skips a builder already terminated by snapshot creation', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + builder_session_id: 'builder-snapshotted', + validator_session_id: 'validator-active', + snapshot_id: 'snapshot-ready', + }); + const state = storedRow(fixture); + if (!state) throw new Error('Expected active validator'); + + await fixture.build.cleanup(startInput()); + + expect(mocks.client.stopSession.mock.calls).toEqual([ + ['validator-active', state.validator_name], + ]); + expect(mocks.client.deleteSnapshot).toHaveBeenCalledWith('snapshot-ready'); + expect(storedRow(fixture)).toBeUndefined(); + }); + + it.each(['stopped', 'failed', 'aborted'])( + 'accepts the terminal provider stop state %s', + async status => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { builder_session_id: 'builder-terminal' }); + mocks.client.stopSession.mockResolvedValueOnce({ status }); + + await fixture.build.cleanup(startInput()); + + expect(storedRow(fixture)).toBeUndefined(); + expect(fixture.storage.deleteAlarm).toHaveBeenCalledOnce(); + } + ); + + it.each([404, 410])('treats provider stop status %s as already terminal', async status => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { builder_session_id: 'builder-absent' }); + mocks.client.stopSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'stop-session', status) + ); + + await fixture.build.cleanup(startInput()); + + expect(storedRow(fixture)).toBeUndefined(); + expect(fixture.storage.deleteAlarm).toHaveBeenCalledOnce(); + }); + + it.each([403, 503])( + 'propagates provider stop status %s without losing the session or alarm', + async status => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { builder_session_id: 'builder-retained' }); + mocks.client.stopSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'stop-session', status) + ); + + await expect(fixture.build.cleanup(startInput())).rejects.toMatchObject({ + operation: 'stop-session', + status, + }); + + expect(storedRow(fixture)?.builder_session_id).toBe('builder-retained'); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + } + ); + + it('rejects a nonterminal stop result and retains cleanup ownership', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { validator_session_id: 'validator-running' }); + mocks.client.stopSession.mockResolvedValueOnce({ status: 'stopping' }); + + await expect(fixture.build.cleanup(startInput())).rejects.toMatchObject({ + kind: 'request_failed', + operation: 'stop-session', + }); + + expect(storedRow(fixture)?.validator_session_id).toBe('validator-running'); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + }); + + it('retains stopped-session references when later snapshot deletion fails', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + builder_session_id: 'builder-retry', + snapshot_candidate_id: 'snapshot-retry', + }); + mocks.client.deleteSnapshot.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'delete-snapshot', 503) + ); + + await expect(fixture.build.cleanup(startInput())).rejects.toMatchObject({ + operation: 'delete-snapshot', + status: 503, + }); + expect(storedRow(fixture)).toMatchObject({ + builder_session_id: 'builder-retry', + snapshot_candidate_id: 'snapshot-retry', + }); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + + mocks.client.stopSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'stop-session', 404) + ); + await fixture.build.cleanup(startInput()); + + expect(mocks.client.stopSession).toHaveBeenCalledTimes(2); + expect(mocks.client.deleteSnapshot).toHaveBeenCalledTimes(2); + expect(storedRow(fixture)).toBeUndefined(); + }); + + it('deletes a successful PostgreSQL snapshot without requiring local build state', async () => { + const fixture = createFixture(); + const input = { ...startInput(), snapshotId: 'snapshot-ready' }; + + await fixture.build.cleanup(input); + mocks.client.deleteSnapshot.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'delete-snapshot', 404) + ); + await expect(fixture.build.cleanup(input)).resolves.toBeUndefined(); + + expect(mocks.client.deleteSnapshot).toHaveBeenCalledTimes(2); + expect(mocks.client.deleteSnapshot).toHaveBeenCalledWith('snapshot-ready'); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + }); + + it('deduplicates persisted IDs and reconciles in-flight snapshots before erasing its owner', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'snapshot_builder', + snapshot_candidate_id: 'snapshot-candidate', + snapshot_source_session_id: 'builder-session', + snapshot_baseline_json: JSON.stringify(['snapshot-existing']), + snapshot_requested: 2, + }); + mocks.client.listSnapshots.mockResolvedValueOnce([ + { id: 'snapshot-existing', sourceSessionId: 'builder-session', status: 'created' }, + { id: 'snapshot-candidate', sourceSessionId: 'builder-session', status: 'created' }, + { id: 'snapshot-recovered', sourceSessionId: 'builder-session', status: 'created' }, + { id: 'snapshot-unrelated', sourceSessionId: 'other-session', status: 'created' }, + ]); + + await fixture.build.cleanup({ ...startInput(), snapshotId: 'snapshot-candidate' }); + + expect(mocks.client.deleteSnapshot.mock.calls.map(([snapshotId]) => snapshotId).sort()).toEqual( + ['snapshot-candidate', 'snapshot-recovered'] + ); + expect(storedRow(fixture)).toBeUndefined(); + expect(fixture.storage.deleteAlarm).toHaveBeenCalledOnce(); + }); + + it('never lets stale cleanup delete a newer snapshot or cancel its alarm', async () => { + const fixture = createFixture(); + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + await fixture.build.start(startInput(SECOND_GENERATION)); + updateRow(fixture, { snapshot_id: 'snapshot-newer' }); + mocks.fetchCredential.mockClear(); + + await fixture.build.cleanup({ ...startInput(), snapshotId: 'snapshot-newer' }); + + expect(mocks.fetchCredential).not.toHaveBeenCalled(); + expect(mocks.client.deleteSnapshot).not.toHaveBeenCalled(); + expect(storedRow(fixture)?.snapshot_id).toBe('snapshot-newer'); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + }); + + it('does not cancel a replacement alarm when cleanup loses ownership during provider I/O', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { snapshot_id: 'snapshot-superseded' }); + + const deletion = deferred(); + mocks.client.deleteSnapshot.mockImplementationOnce(() => deletion.promise); + const staleCleanup = fixture.build.cleanup(startInput()); + await vi.waitFor(() => expect(mocks.client.deleteSnapshot).toHaveBeenCalledOnce()); + + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + await fixture.build.start(startInput(SECOND_GENERATION)); + const replacementAlarmCount = fixture.storage.setAlarm.mock.calls.length; + deletion.resolve(); + await staleCleanup; + + expect(storedRow(fixture)?.build_generation).toBe(SECOND_GENERATION); + expect(fixture.storage.setAlarm).toHaveBeenCalledTimes(replacementAlarmCount); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + }); + + it('never stops builder or validator sessions owned by a newer generation', async () => { + const fixture = createFixture(); + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + await fixture.build.start(startInput(SECOND_GENERATION)); + updateRow(fixture, { + builder_session_id: 'builder-newer', + validator_session_id: 'validator-newer', + }); + + await fixture.build.cleanup(startInput()); + + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + builder_session_id: 'builder-newer', + validator_session_id: 'validator-newer', + }); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + }); + + it('stops processing stale session cleanup after ownership changes during provider I/O', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + builder_session_id: 'builder-old', + validator_session_id: 'validator-old', + }); + + const stopping = deferred<{ status: string }>(); + mocks.client.stopSession.mockImplementationOnce(() => stopping.promise); + const staleCleanup = fixture.build.cleanup(startInput()); + await vi.waitFor(() => expect(mocks.client.stopSession).toHaveBeenCalledOnce()); + + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + await fixture.build.start(startInput(SECOND_GENERATION)); + updateRow(fixture, { + builder_session_id: 'builder-newer', + validator_session_id: 'validator-newer', + }); + stopping.resolve({ status: 'stopped' }); + await staleCleanup; + + expect(mocks.client.stopSession.mock.calls.map(([sessionId]) => sessionId)).not.toContain( + 'builder-newer' + ); + expect(mocks.client.stopSession.mock.calls.map(([sessionId]) => sessionId)).not.toContain( + 'validator-newer' + ); + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + builder_session_id: 'builder-newer', + validator_session_id: 'validator-newer', + }); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + }); + + it('erases only its own state when credentials are missing without issuing provider cleanup', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + builder_session_id: 'builder-unreachable', + validator_session_id: 'validator-unreachable', + snapshot_id: 'snapshot-unreachable', + }); + mocks.fetchCredential.mockRejectedValueOnce(new mocks.CredentialMissingError('removed')); + + await fixture.build.cleanup({ ...startInput(), snapshotId: 'snapshot-unreachable' }); + + expect(mocks.resolveAccess).not.toHaveBeenCalled(); + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + expect(mocks.client.deleteSnapshot).not.toHaveBeenCalled(); + expect(storedRow(fixture)).toBeUndefined(); + expect(fixture.storage.deleteAlarm).toHaveBeenCalledOnce(); + }); + + it('does not attempt provider stop when an alarm discovers credentials were removed', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { builder_session_id: 'builder-unreachable' }); + mocks.fetchCredential.mockRejectedValueOnce(new mocks.CredentialMissingError('removed')); + + await fixture.build.alarm(); + + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + expect(storedRow(fixture)).toBeUndefined(); + expect(fixture.storage.deleteAlarm).toHaveBeenCalledOnce(); + }); + + it('stops a failed validator and deletes its snapshot without restopping the builder', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'verify_validator_call_home', + builder_session_id: 'builder-snapshotted', + validator_session_id: 'validator-failed', + validator_wrapper_command_id: 'validator-command', + validator_control_id: 'validator-control', + snapshot_id: 'snapshot-failed', + }); + const state = storedRow(fixture); + if (!state) throw new Error('Expected active validator'); + mocks.client.getCommand.mockResolvedValueOnce({ exitCode: 1 }); + + await fixture.build.alarm(); + await runNextAlarm(fixture); + + expect(mocks.projectStatus).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ setupStatus: 'failed' }) + ); + expect(mocks.client.stopSession.mock.calls).toEqual([ + ['validator-failed', state.validator_name], + ]); + expect(mocks.client.deleteSnapshot).toHaveBeenCalledWith('snapshot-failed'); + expect(storedRow(fixture)).toBeUndefined(); + }); + + it('replays failed builder cleanup from its alarm without rerunning the failed build step', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { builder_session_id: 'builder-failed' }); + mocks.client.inspectTeam.mockRejectedValueOnce(new Error('setup_command_failed')); + mocks.client.stopSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'stop-session', 503) + ); + + await fixture.build.alarm(); + expect(storedRow(fixture)).toMatchObject({ + step: 'project_failure', + last_error: 'setup_command_failed', + next_attempt_at: expect.any(Number), + }); + await runNextAlarm(fixture); + + expect(storedRow(fixture)).toMatchObject({ + step: 'cleanup', + builder_session_id: 'builder-failed', + last_error: 'setup_command_failed', + next_attempt_at: expect.any(Number), + }); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + + await runNextAlarm(fixture); + + expect(mocks.client.inspectTeam).toHaveBeenCalledOnce(); + expect(mocks.projectStatus).toHaveBeenCalledOnce(); + expect(mocks.client.stopSession).toHaveBeenCalledTimes(2); + expect(storedRow(fixture)).toBeUndefined(); + expect(await fixture.storage.getAlarm()).toBeNull(); + }); + + it('deletes failed snapshots while their credential remains available', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { snapshot_id: 'snapshot-failed' }); + mocks.client.inspectTeam.mockRejectedValueOnce(new Error('deterministic-failure')); + + await fixture.build.alarm(); + await runNextAlarm(fixture); + + expect(mocks.projectStatus).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ setupStatus: 'failed', runtimeSnapshotId: null }) + ); + expect(mocks.client.deleteSnapshot).toHaveBeenCalledWith('snapshot-failed'); + expect(storedRow(fixture)).toBeUndefined(); + expect(fixture.storage.deleteAlarm).toHaveBeenCalledOnce(); + }); +}); + +describe('VercelSnapshotBuild terminal replay', () => { + it('replays projection and cleanup after restart beyond the original build lifetime without changing the public failure', async () => { + const startedAt = Date.now(); + const clock = vi.spyOn(Date, 'now').mockReturnValue(startedAt); + try { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'verify_validator_call_home', + snapshot_id: 'snapshot-overdue', + validator_session_id: 'validator-overdue', + validator_wrapper_command_id: 'validator-command', + validator_control_id: 'validator-control', + }); + mocks.client.getCommand.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'get-command', 403) + ); + await fixture.build.alarm(); + const failed = storedRow(fixture); + if (!failed) throw new Error('Expected durable terminal projection'); + mocks.projectStatus.mockRejectedValueOnce(new mocks.CredentialResolverError('unavailable')); + mocks.client.stopSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'stop-session', 503) + ); + mocks.client.deleteSnapshot.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'delete-snapshot', 503) + ); + + for (const [index, step] of ['project_failure', 'cleanup', 'cleanup'].entries()) { + clock.mockReturnValue(startedAt + (index + 1) * 2 * 60 * 60_000); + fixture.build = createBuild(fixture.storage); + await fixture.build.alarm(); + const retained = storedRow(fixture); + expect(retained).toMatchObject({ + step, + created_at: startedAt, + build_generation: FIRST_GENERATION, + last_error: 'byoc_vercel_forbidden', + snapshot_id: failed.snapshot_id, + validator_session_id: failed.validator_session_id, + validator_name: failed.validator_name, + validator_operation_id: failed.validator_operation_id, + validator_control_id: failed.validator_control_id, + }); + expect(retained?.next_attempt_at).toBeGreaterThan(Date.now()); + expect(await fixture.storage.getAlarm()).toBe(retained?.next_attempt_at); + } + + clock.mockReturnValue(startedAt + 8 * 60 * 60_000); + fixture.build = createBuild(fixture.storage); + mocks.client.stopSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'stop-session', 404) + ); + await fixture.build.alarm(); + + expect(storedRow(fixture)).toBeUndefined(); + expect(await fixture.storage.getAlarm()).toBeNull(); + expect(mocks.client.getCommand).toHaveBeenCalledOnce(); + expect(mocks.client.createSandbox).not.toHaveBeenCalled(); + expect(mocks.client.stopSession.mock.calls).toEqual([ + ['validator-overdue', failed.validator_name], + ['validator-overdue', failed.validator_name], + ['validator-overdue', failed.validator_name], + ]); + expect(mocks.client.deleteSnapshot.mock.calls).toEqual([ + ['snapshot-overdue'], + ['snapshot-overdue'], + ]); + expect(mocks.projectStatus).toHaveBeenCalledTimes(2); + for (const [, projection] of mocks.projectStatus.mock.calls) { + expect(projection).toMatchObject({ + organizationId: ORGANIZATION_ID, + credentialId: CREDENTIAL_ID, + buildGeneration: FIRST_GENERATION, + setupStatus: 'failed', + setupStep: null, + setupError: 'byoc_vercel_forbidden', + runtimeSnapshotId: null, + setupStartedAt: new Date(startedAt).toISOString(), + setupCompletedAt: null, + }); + } + } finally { + clock.mockRestore(); + } + }); + + it('retains the original safe failure and resources through repeated projection failures', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'verify_validator_call_home', + snapshot_id: 'snapshot-failed', + validator_session_id: 'validator-failed', + validator_wrapper_command_id: 'validator-command', + validator_control_id: 'validator-control', + }); + mocks.client.getCommand.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'get-command', 403) + ); + await fixture.build.alarm(); + const failed = storedRow(fixture); + mocks.projectStatus.mockRejectedValue(new mocks.CredentialResolverError('unavailable')); + + for (let attempt = 0; attempt < 8; attempt += 1) { + await runNextAlarm(fixture); + expect(storedRow(fixture)).toMatchObject({ + step: 'project_failure', + build_generation: FIRST_GENERATION, + snapshot_id: 'snapshot-failed', + validator_session_id: 'validator-failed', + validator_name: failed?.validator_name, + last_error: 'byoc_vercel_forbidden', + next_attempt_at: expect.any(Number), + }); + } + expect(mocks.client.getCommand).toHaveBeenCalledOnce(); + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + expect(mocks.client.deleteSnapshot).not.toHaveBeenCalled(); + expect( + mocks.projectStatus.mock.calls.every( + ([, projection]) => + projection.setupStatus === 'failed' && projection.setupError === 'byoc_vercel_forbidden' + ) + ).toBe(true); + + mocks.projectStatus.mockResolvedValue(true); + await runNextAlarm(fixture); + + expect(mocks.client.stopSession).toHaveBeenCalledWith( + 'validator-failed', + failed?.validator_name + ); + expect(mocks.client.deleteSnapshot).toHaveBeenCalledWith('snapshot-failed'); + expect(storedRow(fixture)).toBeUndefined(); + expect(await fixture.storage.getAlarm()).toBeNull(); + }); + + it('retries snapshot deletion after the failure projection and validator stop are complete', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'verify_validator_call_home', + snapshot_id: 'snapshot-failed', + validator_session_id: 'validator-failed', + validator_wrapper_command_id: 'validator-command', + validator_control_id: 'validator-control', + }); + mocks.client.getCommand.mockResolvedValueOnce({ exitCode: 1 }); + mocks.client.deleteSnapshot.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'delete-snapshot', 503) + ); + await fixture.build.alarm(); + await runNextAlarm(fixture); + + expect(storedRow(fixture)).toMatchObject({ + step: 'cleanup', + snapshot_id: 'snapshot-failed', + validator_session_id: 'validator-failed', + last_error: 'validator_call_home_failed', + next_attempt_at: expect.any(Number), + }); + expect(mocks.control.confirmSnapshotValidatorStopped).toHaveBeenCalledOnce(); + mocks.client.stopSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'stop-session', 404) + ); + await runNextAlarm(fixture); + + expect(mocks.projectStatus).toHaveBeenCalledOnce(); + expect(mocks.client.getCommand).toHaveBeenCalledOnce(); + expect(mocks.client.deleteSnapshot.mock.calls).toEqual([ + ['snapshot-failed'], + ['snapshot-failed'], + ]); + expect(storedRow(fixture)).toBeUndefined(); + }); + + it('continues abandoned-generation cleanup without projecting a new failure', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { builder_session_id: 'builder-superseded' }); + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + mocks.client.stopSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'stop-session', 503) + ); + + await fixture.build.alarm(); + expect(storedRow(fixture)).toMatchObject({ + step: 'cleanup', + build_generation: FIRST_GENERATION, + builder_session_id: 'builder-superseded', + next_attempt_at: expect.any(Number), + }); + await runNextAlarm(fixture); + + expect(mocks.projectStatus).not.toHaveBeenCalled(); + expect(mocks.client.inspectTeam).not.toHaveBeenCalled(); + expect(mocks.client.stopSession).toHaveBeenCalledTimes(2); + expect(storedRow(fixture)).toBeUndefined(); + }); + + it('does not let a delayed terminal projection touch replacement resources or scheduling', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { snapshot_id: 'snapshot-old' }); + mocks.client.inspectTeam.mockRejectedValueOnce(new Error('setup_command_failed')); + await fixture.build.alarm(); + const projection = deferred(); + mocks.projectStatus.mockImplementationOnce(() => projection.promise); + const staleAlarm = runNextAlarm(fixture); + await vi.waitFor(() => expect(mocks.projectStatus).toHaveBeenCalledOnce()); + + mocks.fetchCredential.mockResolvedValue(makeCredential(SECOND_GENERATION)); + await fixture.build.start(startInput(SECOND_GENERATION)); + updateRow(fixture, { snapshot_id: 'snapshot-new', validator_session_id: 'validator-new' }); + const alarmCount = fixture.storage.setAlarm.mock.calls.length; + projection.reject(new mocks.CredentialResolverError('unavailable')); + await staleAlarm; + + expect(storedRow(fixture)).toMatchObject({ + build_generation: SECOND_GENERATION, + step: 'validating_access', + snapshot_id: 'snapshot-new', + validator_session_id: 'validator-new', + }); + expect(mocks.client.deleteSnapshot.mock.calls).toEqual([['snapshot-old']]); + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + expect(fixture.storage.setAlarm).toHaveBeenCalledTimes(alarmCount); + expect(fixture.storage.deleteAlarm).not.toHaveBeenCalled(); + }); + + it('erases local terminal state when credentials disappear without attempting provider cleanup', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { builder_session_id: 'builder-inaccessible' }); + mocks.client.inspectTeam.mockRejectedValueOnce(new Error('setup_command_failed')); + await fixture.build.alarm(); + mocks.fetchCredential.mockRejectedValue(new mocks.CredentialMissingError('removed')); + + await runNextAlarm(fixture); + + expect(storedRow(fixture)).toBeUndefined(); + expect(mocks.client.stopSession).not.toHaveBeenCalled(); + expect(mocks.client.deleteSnapshot).not.toHaveBeenCalled(); + expect(await fixture.storage.getAlarm()).toBeNull(); + }); +}); + +describe('VercelSnapshotBuild runtime preparation', () => { + it('registers the fenced validator allocation before launching its matching wrapper identity', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'launch_validator_wrapper', + snapshot_id: 'snapshot-validator', + validator_session_id: 'validator-session', + }); + const state = storedRow(fixture); + if (!state) throw new Error('Expected validator state'); + const createdAt = Date.now() - 1_000; + mocks.client.getSession.mockResolvedValueOnce({ + session: { status: 'running', requestedAt: createdAt, timeout: 600_000 }, + }); + mocks.control.initializeSnapshotValidator.mockImplementationOnce(async () => { + expect(storedRow(fixture)).toMatchObject({ + validator_control_id: `ses-byoc-validator-${FIRST_GENERATION.replaceAll('-', '')}`, + validator_wrapper_requested: 0, + }); + expect(mocks.client.executeCommand).not.toHaveBeenCalled(); + }); + + await fixture.build.alarm(); + + const providerRef = encodeVercelProviderRef({ + sandboxName: state.validator_name, + sessionId: 'validator-session', + }); + const launch = mocks.client.executeCommand.mock.calls[0]?.[1]; + const identity = verifySandboxLaunchCredential( + launch.env.SANDBOX_CONTROL_CREDENTIAL, + 'snapshot-test-signing-secret' + ); + expect(identity?.allocationId).toBe(FIRST_GENERATION); + if (!identity) throw new Error('Expected signed validator identity'); + expect(launch.env.PROVIDER_INSTANCE_ID).toBe(providerRef); + expect(mocks.control.initializeSnapshotValidator).toHaveBeenCalledWith({ + build: { + organizationId: ORGANIZATION_ID, + credentialId: CREDENTIAL_ID, + generation: FIRST_GENERATION, + }, + allocation: { + providerRef, + locator: { + teamId: 'team-test', + projectId: 'project-test', + snapshotId: 'snapshot-validator', + runtimeBuildId: state.runtime_build_id, + runtime: 'node24', + }, + createdAt, + expiresAt: createdAt + 600_000, + }, + credentialHash: await hashSandboxCredential(identity.credential), + }); + expect(storedRow(fixture)).toMatchObject({ + step: 'verify_validator_call_home', + validator_wrapper_requested: 1, + validator_wrapper_command_id: 'command-test', + }); + }); + + it('passes persisted personal ownership to validator initialization', async () => { + const database = new DatabaseSync(':memory:'); + const storage = createStorage(database); + const build = createBuild(storage, `user:${PERSONAL_USER_ID}`); + const input = startInput(FIRST_GENERATION, { userId: PERSONAL_USER_ID }); + await build.start(input); + updateRow( + { database, storage, build }, + { + step: 'launch_validator_wrapper', + snapshot_id: 'snapshot-validator', + validator_session_id: 'validator-session', + }, + `user:${PERSONAL_USER_ID}` + ); + + await build.alarm(); + + expect(mocks.control.initializeSnapshotValidator).toHaveBeenCalledWith( + expect.objectContaining({ + build: { + userId: PERSONAL_USER_ID, + credentialId: CREDENTIAL_ID, + generation: FIRST_GENERATION, + }, + }) + ); + }); + + it('waits for the validator allocation to run before registering or launching the wrapper', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'launch_validator_wrapper', + snapshot_id: 'snapshot-validator', + validator_session_id: 'validator-session', + }); + mocks.client.getSession.mockResolvedValueOnce({ session: { status: 'pending' } }); + + await fixture.build.alarm(); + + expect(storedRow(fixture)).toMatchObject({ + step: 'launch_validator_wrapper', + validator_wrapper_requested: 0, + }); + expect(mocks.control.initializeSnapshotValidator).not.toHaveBeenCalled(); + expect(mocks.client.executeCommand).not.toHaveBeenCalled(); + await runNextAlarm(fixture); + expect(mocks.control.initializeSnapshotValidator).toHaveBeenCalledOnce(); + expect(mocks.client.executeCommand).toHaveBeenCalledOnce(); + }); + + it('cleans up a validator whose control failed readiness even while its command still runs', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'verify_validator_call_home', + snapshot_id: 'snapshot-validator', + validator_session_id: 'validator-session', + validator_control_id: 'validator-control', + validator_wrapper_command_id: 'validator-command', + }); + mocks.client.getCommand.mockResolvedValueOnce({ exitCode: null }); + mocks.control.getSnapshotValidatorStatus.mockResolvedValueOnce({ + physical: 'failed', + connection: 'disconnected', + }); + + await fixture.build.alarm(); + expect(storedRow(fixture)).toMatchObject({ + step: 'project_failure', + last_error: 'validator_call_home_failed', + }); + await runNextAlarm(fixture); + + expect(mocks.client.getCommand).toHaveBeenCalledOnce(); + expect(mocks.client.stopSession).toHaveBeenCalledOnce(); + expect(mocks.client.deleteSnapshot).toHaveBeenCalledWith('snapshot-validator'); + expect(storedRow(fixture)).toBeUndefined(); + }); + + it('recovers an ambiguous validator launch without rotating its credential or launching twice', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'launch_validator_wrapper', + snapshot_id: 'snapshot-validator', + validator_session_id: 'validator-session', + }); + mocks.client.executeCommand.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'execute-command', 503) + ); + await fixture.build.alarm(); + mocks.client.listCommands.mockResolvedValueOnce([ + { id: 'recovered-command', args: [`byoc-call-home:${FIRST_GENERATION}`] }, + ]); + + await runNextAlarm(fixture); + + expect(mocks.control.initializeSnapshotValidator).toHaveBeenCalledOnce(); + expect(mocks.client.executeCommand).toHaveBeenCalledOnce(); + expect(mocks.client.createSandbox).not.toHaveBeenCalled(); + expect(storedRow(fixture)).toMatchObject({ + step: 'verify_validator_call_home', + validator_wrapper_command_id: 'recovered-command', + }); + }); + + it('retries validator registration before recording a wrapper launch intent', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'launch_validator_wrapper', + snapshot_id: 'snapshot-validator', + validator_session_id: 'validator-session', + }); + mocks.control.initializeSnapshotValidator.mockRejectedValueOnce( + new mocks.CredentialResolverError('registration unavailable') + ); + await fixture.build.alarm(); + + expect(storedRow(fixture)).toMatchObject({ + step: 'launch_validator_wrapper', + validator_wrapper_requested: 0, + }); + expect(mocks.client.executeCommand).not.toHaveBeenCalled(); + await runNextAlarm(fixture); + + expect(mocks.control.initializeSnapshotValidator).toHaveBeenCalledTimes(2); + expect(mocks.client.executeCommand).toHaveBeenCalledOnce(); + expect(mocks.client.createSandbox).not.toHaveBeenCalled(); + }); + + it('deletes the previous Ready snapshot after projecting a replacement', async () => { + const fixture = createFixture(); + mocks.fetchCredential.mockResolvedValue({ + ...makeCredential(), + runtimeSnapshotId: 'snapshot-old', + }); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'confirm_terminal', + snapshot_id: 'snapshot-ready', + validator_session_id: 'validator-session', + }); + mocks.client.getSession.mockResolvedValueOnce({ session: { status: 'stopped' } }); + + await fixture.build.alarm(); + + expect(mocks.projectStatus).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + setupStatus: 'ready', + runtimeSnapshotId: 'snapshot-ready', + runtimeDigest: 'digest-test', + }) + ); + expect(mocks.client.deleteSnapshot).toHaveBeenCalledWith('snapshot-old'); + expect(storedRow(fixture)).toBeUndefined(); + }); + + it.each([404, 410])( + 'keeps the validated snapshot when terminal inspection returns %s', + async status => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'confirm_terminal', + snapshot_id: 'snapshot-ready', + validator_session_id: 'validator-session', + }); + mocks.client.getSession.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'get-session', status) + ); + + await runNextAlarm(fixture); + + expect(mocks.projectStatus).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ setupStatus: 'ready', runtimeSnapshotId: 'snapshot-ready' }) + ); + expect(mocks.client.deleteSnapshot).not.toHaveBeenCalled(); + expect(storedRow(fixture)).toBeUndefined(); + } + ); + + it.each([403, 503])( + 'keeps the new ready snapshot if superseded snapshot deletion returns %s', + async status => { + const fixture = createFixture(); + mocks.fetchCredential.mockResolvedValue({ + ...makeCredential(), + runtimeSnapshotId: 'snapshot-old', + }); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'confirm_terminal', + snapshot_id: 'snapshot-ready', + validator_session_id: 'validator-session', + }); + mocks.client.getSession.mockResolvedValue({ session: { status: 'stopped' } }); + mocks.client.deleteSnapshot.mockRejectedValueOnce( + new mocks.SandboxRestError('request_failed', 'delete-snapshot', status) + ); + + await runNextAlarm(fixture); + + expect(mocks.projectStatus).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ setupStatus: 'ready', runtimeSnapshotId: 'snapshot-ready' }) + ); + expect(mocks.client.deleteSnapshot.mock.calls).toEqual([['snapshot-old']]); + expect(storedRow(fixture)).toBeUndefined(); + } + ); + + it('resets projection retries after a full step succeeds', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + mocks.projectStatus.mockRejectedValueOnce(new mocks.CredentialResolverError('unavailable')); + await runNextAlarm(fixture); + expect(storedRow(fixture)).toMatchObject({ step: 'validating_access', retry_count: 1 }); + await runNextAlarm(fixture); + expect(storedRow(fixture)).toMatchObject({ step: 'create_builder', retry_count: 0 }); + }); + + it('backs off and exhausts projection retries after successful terminal inspection', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'confirm_terminal', + snapshot_id: 'snapshot-ready', + validator_session_id: 'validator-session', + }); + mocks.client.getSession.mockResolvedValue({ session: { status: 'stopped' } }); + mocks.projectStatus.mockRejectedValue(new mocks.CredentialResolverError('unavailable')); + + for (const [index, delay] of [1_000, 5_000, 15_000, 30_000, 60_000].entries()) { + const before = Date.now(); + await runNextAlarm(fixture); + expect(storedRow(fixture)).toMatchObject({ + step: 'confirm_terminal', + retry_count: index + 1, + }); + expect(storedRow(fixture)?.next_attempt_at).toBeGreaterThanOrEqual(before + delay); + } + await runNextAlarm(fixture); + expect(storedRow(fixture)).toMatchObject({ step: 'project_failure' }); + + mocks.projectStatus.mockResolvedValue(true); + await runNextAlarm(fixture); + expect(mocks.projectStatus).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ setupStatus: 'failed' }) + ); + expect(storedRow(fixture)).toBeUndefined(); + }); + + it('projects a ready snapshot without deleting it when the validator is terminal', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'confirm_terminal', + snapshot_id: 'snapshot-ready', + validator_session_id: 'validator-session', + }); + mocks.client.getSession.mockResolvedValueOnce({ session: { status: 'stopped' } }); + + await fixture.build.alarm(); + + expect(mocks.projectStatus).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ setupStatus: 'ready', runtimeSnapshotId: 'snapshot-ready' }) + ); + expect(mocks.client.deleteSnapshot).not.toHaveBeenCalled(); + expect(storedRow(fixture)).toBeUndefined(); + expect(fixture.storage.deleteAlarm).toHaveBeenCalledOnce(); + }); + + it('creates snapshots with an explicit thirty-day expiration in milliseconds', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'snapshot_builder', + snapshot_source_session_id: 'builder-session', + snapshot_baseline_json: '[]', + snapshot_requested: 1, + }); + mocks.client.createSnapshot.mockResolvedValueOnce({ + id: 'snapshot-finite', + sourceSessionId: 'builder-session', + status: 'created', + }); + + await fixture.build.alarm(); + + expect(mocks.client.createSnapshot).toHaveBeenCalledWith('builder-session', 2_592_000_000); + expect(storedRow(fixture)).toMatchObject({ + step: 'create_validator', + snapshot_id: 'snapshot-finite', + }); + }); + + it('prepares a workspace owned and writable by the sandbox user', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'install_system_dependencies', + builder_session_id: 'builder-session', + }); + + await fixture.build.alarm(); + + const command = mocks.client.executeCommand.mock.calls[0]?.[1]; + expect(command.args[1]).toContain('sudo mkdir -p /workspace'); + expect(command.args[1]).toContain('sudo chown "$(id -u):$(id -g)" /workspace'); + expect(command.args[1]).toContain('test -w /workspace'); + }); + + it('stages runtime artifacts in tmp and installs validated root-owned files with sudo', async () => { + const fixture = createFixture(); + await fixture.build.start(startInput()); + updateRow(fixture, { + step: 'upload_runtime_artifacts', + builder_session_id: 'builder-session', + }); + + await fixture.build.alarm(); + + expect(mocks.client.writeFiles).toHaveBeenCalledWith('builder-session', '/tmp', [ + expect.objectContaining({ path: 'kilocode-control-plane-wrapper.js' }), + expect.objectContaining({ path: 'kilocode-control-plane-supervisor.sh' }), + expect.objectContaining({ path: 'kilo-runtime-manifest.json' }), + ]); + const manifest = JSON.parse( + mocks.client.writeFiles.mock.calls[0]?.[2]?.find( + (file: { path: string }) => file.path === 'kilo-runtime-manifest.json' + )?.content + ); + expect(manifest.controlWrapperSha256).toBe('b'.repeat(64)); + + await fixture.build.alarm(); + + const command = mocks.client.executeCommand.mock.calls[0]?.[1]; + expect(command.args[1]).toContain( + 'sudo install -m 0755 /tmp/kilocode-control-plane-wrapper.js /usr/local/bin/kilocode-control-plane-wrapper.js' + ); + expect(command.args[1]).toContain( + 'sudo install -m 0644 /tmp/kilo-runtime-manifest.json /usr/local/share/kilo/runtime-manifest.json' + ); + expect(command.args[1]).toContain('sha256sum /usr/local/bin/kilocode-control-plane-wrapper.js'); + }); +}); diff --git a/services/cloud-agent-next/src/persistence/VercelSnapshotBuild.ts b/services/cloud-agent-next/src/persistence/VercelSnapshotBuild.ts new file mode 100644 index 00000000000..af293796dd0 --- /dev/null +++ b/services/cloud-agent-next/src/persistence/VercelSnapshotBuild.ts @@ -0,0 +1,1569 @@ +import { + ownerToRowKey, + ownerFromRow, + ownerToProjection, + ownerFromInput, + ownerCredentialFetchInput, +} from '../byoc/vercel-snapshot-owner.js'; +import { byocVercelOwnerSchema } from '../byoc/vercel-owner.js'; +import { DurableObject } from 'cloudflare:workers'; +import { drizzle } from 'drizzle-orm/durable-sqlite'; +import { migrate } from 'drizzle-orm/durable-sqlite/migrator'; +import { and, eq } from 'drizzle-orm'; +import { z } from 'zod'; + +import migrations from '../../drizzle/vercel-snapshot/migrations'; +import { vercelSnapshotBuilds } from '../db/vercel-snapshot-schema.js'; +import type { Env } from '../types.js'; +import { + ByocCredentialMissingError, + ByocCredentialResolverError, + fetchByocVercelCredential, + projectByocVercelStatus, + resolveByocVercelAccessConfig, + type ByocVercelCredential, +} from '../byoc/vercel-credential-resolver.js'; +import { getVercelRuntimeArtifacts } from '../byoc/vercel-runtime-artifacts.js'; +import { + getVercelRuntimeIdentity, + VERCEL_RUNTIME_BUN_VERSION, + VERCEL_RUNTIME_KILO_CLI_VERSION, +} from '../byoc/vercel-runtime-identity.js'; +import { + VERCEL_CLOUD_AGENT_CREATE_OPERATION_TAG, + VERCEL_CLOUD_AGENT_RESOURCE_TAG, + VERCEL_CLOUD_AGENT_RESOURCE_TAG_VALUE, + VERCEL_CLOUD_AGENT_RUNTIME_BUILD_TAG, + VercelSandboxRestClient, + VercelSandboxRestError, + type CreateSandboxInput, + type VercelSandboxCommand, + type VercelSandboxCreateEnvelope, + type VercelSandboxSnapshot, +} from '../agent-sandbox/vercel/vercel-sandbox-rest-client.js'; +import { WRAPPER_VERSION } from '../shared/wrapper-version.js'; +import { logger } from '../logger.js'; +import { getSandboxControlStub } from '../sandbox-control/stub.js'; +import { + generateSandboxCredential, + hashSandboxCredential, + mintSandboxLaunchCredential, +} from '../sandbox-control/credential.js'; +import { buildControlWrapperLaunchEnv } from '../sandbox-control/wrapper-launch-env.js'; +import { encodeVercelProviderRef } from '../sandbox-control/vercel-provider.js'; +import type { VercelSandboxRuntimeConfig } from '../agent-sandbox/vercel/vercel-runtime-config.js'; +import { resolveSecret } from '../auth.js'; +import { CONTROL_PLANE_ALLOCATION_ID_ENV } from '../shared/control-plane-protocol.js'; +import { CONTROL_SUPERVISOR_PATH } from '../sandbox-control/container-paths.js'; +import { VERCEL_BASE_SYSTEM_SETUP, VERCEL_BASE_NODE_SETUP } from '../byoc/vercel-runtime-base.js'; +import { withDORetry } from '../utils/do-retry.js'; + +const BUILD_ALARM_DELAY_MS = 1_000; +const RETRY_DELAY_MS = [1_000, 5_000, 15_000, 30_000, 60_000] as const; +const MAX_RETRY_DELAY_MS = 60_000; +const MAX_RETRIES = RETRY_DELAY_MS.length; +const BUILDER_TIMEOUT_MS = 10 * 60_000; +const SNAPSHOT_EXPIRATION_MS = 30 * 24 * 60 * 60 * 1_000; +const PINNED_BUN_VERSION = VERCEL_RUNTIME_BUN_VERSION; +const PINNED_KILO_VERSION = VERCEL_RUNTIME_KILO_CLI_VERSION; + +const SnapshotBuildOwnerSchema = byocVercelOwnerSchema; + +const StartInputSchema = z + .object({ + credentialId: z.uuid(), + buildGeneration: z.uuid(), + }) + .and(SnapshotBuildOwnerSchema); + +const CleanupInputSchema = z + .object({ + credentialId: z.uuid(), + buildGeneration: z.uuid(), + snapshotId: z.string().min(1).optional(), + }) + .and(SnapshotBuildOwnerSchema); + +const BuildStepSchema = z.enum([ + 'validating_access', + 'create_builder', + 'install_system_dependencies', + 'install_node_dependencies', + 'upload_runtime_artifacts', + 'verify_runtime_artifacts', + 'snapshot_builder', + 'create_validator', + 'launch_validator_wrapper', + 'verify_validator_call_home', + 'stop_validator', + 'confirm_terminal', +]); + +type BuildDb = ReturnType; +type BuildRow = typeof vercelSnapshotBuilds.$inferSelect; +type BuildOwnership = Pick< + BuildRow, + 'organization_id' | 'owner_type' | 'credential_id' | 'build_generation' +>; + +class SnapshotBuildOwnershipLostError extends Error { + constructor() { + super('snapshot_build_ownership_lost'); + this.name = 'SnapshotBuildOwnershipLostError'; + } +} + +function now(): number { + return Date.now(); +} + +function iso(timestamp: number | null | undefined): string | null { + return timestamp === null || timestamp === undefined ? null : new Date(timestamp).toISOString(); +} + +function deterministicName(prefix: string, credentialId: string, runtimeBuildId: string): string { + const safeCredential = credentialId.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 8); + const safeRuntime = runtimeBuildId.replace(/[^A-Za-z0-9_-]/g, '-').slice(-32); + return `ses-${prefix}-${safeRuntime}-${safeCredential}`.slice(0, 63); +} + +function operationId(prefix: string, generation: string): string { + return `byoc-${prefix}-${generation}`.slice(0, 256); +} + +function controlId(generation: string): string { + return `ses-byoc-validator-${generation.replace(/-/g, '').slice(0, 32)}`; +} + +function requiredBuildField(value: string | null, error: string): string { + if (!value) throw new Error(error); + return value; +} + +function buildInput(row: BuildRow, source: 'runtime' | 'snapshot') { + const common = { + name: row.builder_name, + operationId: row.builder_operation_id, + runtimeBuildId: row.runtime_build_id, + runtime: 'node24' as const, + timeoutMs: BUILDER_TIMEOUT_MS, + }; + return source === 'runtime' + ? { ...common, source: { type: 'runtime' as const } } + : { + ...common, + source: { + type: 'snapshot' as const, + snapshotId: requiredBuildField(row.snapshot_id, 'snapshot_result_missing'), + }, + }; +} + +function validatorInput(row: BuildRow): CreateSandboxInput { + return { + ...buildInput(row, 'snapshot'), + name: row.validator_name, + operationId: row.validator_operation_id, + }; +} + +const NAMED_BUILD_FAILURES = new Set([ + 'snapshot_result_ambiguous', + 'snapshot_result_missing', + 'setup_command_failed', + 'validator_call_home_failed', + 'validator_call_home_ended', + 'validator_call_home_ambiguous', + 'validator_session_missing', + 'validator_command_missing', + 'validator_control_missing', + 'provider_configuration_failed', +]); + +function failureCode(error: unknown): string { + if (error instanceof ByocCredentialMissingError) return 'byoc_credential_missing'; + if (error instanceof ByocCredentialResolverError) return 'byoc_credential_unusable'; + if (error instanceof VercelSandboxRestError) { + if (error.status === 401 || error.status === 403) return 'byoc_vercel_forbidden'; + if (error.status === 429) return 'byoc_vercel_capacity'; + if (error.kind === 'correlation_mismatch') return 'provider_correlation_failed'; + if (error.kind === 'invalid_response') return 'provider_invalid_response'; + } + if (error instanceof Error && NAMED_BUILD_FAILURES.has(error.message)) return error.message; + return 'provider_request_failed'; +} + +function safeError(error: unknown): string { + return failureCode(error); +} + +function logSnapshotBuildFailure( + state: BuildRow, + error: unknown, + outcome: 'retry' | 'terminal', + extra?: Record +): void { + const owner = ownerFromRow(state); + logger + .withFields({ + logTag: 'byoc_vercel_snapshot_build', + ownerId: owner.type === 'org' ? owner.organizationId : owner.userId, + ownerType: owner.type, + credentialId: state.credential_id, + buildGeneration: state.build_generation, + step: state.step, + outcome, + attempt: state.retry_count + 1, + failureCode: failureCode(error), + errorName: error instanceof Error ? error.name : 'unknown', + ...(error instanceof VercelSandboxRestError + ? { + vercelKind: error.kind, + vercelOperation: error.operation, + vercelStatus: error.status, + } + : {}), + ...extra, + }) + .error('BYOC Vercel snapshot build failed'); +} + +function isRetryable(error: unknown): boolean { + if (error instanceof ByocCredentialMissingError) return false; + if (error instanceof ByocCredentialResolverError) return true; + if (error instanceof VercelSandboxRestError) { + return ( + error.status === undefined || + error.status === 408 || + error.status === 409 || + error.status === 429 || + error.status >= 500 + ); + } + return false; +} + +function snapshotMatches( + snapshot: VercelSandboxSnapshot, + sourceSessionId: string, + baseline: Set +): boolean { + return ( + snapshot.sourceSessionId === sourceSessionId && + !baseline.has(snapshot.id) && + snapshot.status === 'created' + ); +} + +function snapshotBaseline(value: string | null): Set | undefined { + if (!value) return undefined; + + try { + return new Set(z.array(z.string()).parse(JSON.parse(value))); + } catch { + throw new Error('snapshot_baseline_invalid'); + } +} + +function sameOwnership( + first: BuildOwnership | undefined, + second: BuildOwnership | undefined +): boolean { + if (!first || !second) return false; + return ( + ownerToRowKey(ownerFromRow(first)) === ownerToRowKey(ownerFromRow(second)) && + first.credential_id === second.credential_id && + first.build_generation === second.build_generation + ); +} + +function ownershipFromInput(input: z.infer): BuildOwnership { + const owner = ownerFromInput(input); + return { + organization_id: ownerToRowKey(owner), + owner_type: owner.type, + credential_id: input.credentialId, + build_generation: input.buildGeneration, + }; +} + +function ownershipFromRow(row: BuildRow): BuildOwnership { + return { + organization_id: row.organization_id, + owner_type: row.owner_type, + credential_id: row.credential_id, + build_generation: row.build_generation, + }; +} + +export class VercelSnapshotBuild extends DurableObject { + private readonly db: BuildDb; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.db = drizzle(ctx.storage, { logger: false }); + void ctx.blockConcurrencyWhile(async () => { + await migrate(this.db, migrations); + }); + } + + async start(input: z.infer): Promise { + const parsed = StartInputSchema.parse(input); + const owner = ownerFromInput(parsed); + if (ownerToRowKey(owner) !== this.ctx.id.name) { + throw new Error('snapshot_owner_mismatch'); + } + + const observed = this.load(); + const credential = await fetchByocVercelCredential( + this.env, + ownerCredentialFetchInput(owner, parsed.credentialId) + ); + if (credential.buildGeneration !== parsed.buildGeneration) return; + + const ownership = ownershipFromInput(parsed); + const current = this.load(); + if (current && sameOwnership(current, ownership) && current.next_attempt_at !== null) { + await this.arm(ownership, current.next_attempt_at); + return; + } + if (current && !sameOwnership(current, observed)) return; + + const timestamp = now(); + const next = { + organization_id: ownerToRowKey(owner), + owner_type: owner.type, + credential_id: parsed.credentialId, + build_generation: parsed.buildGeneration, + runtime_build_id: credential.runtimeBuildId ?? `vercel-runtime-${parsed.buildGeneration}`, + step: 'validating_access' as const, + retry_count: 0, + next_attempt_at: timestamp, + builder_name: deterministicName( + 'snapshot-builder', + parsed.credentialId, + credential.runtimeBuildId ?? parsed.buildGeneration + ), + builder_operation_id: operationId('builder', parsed.buildGeneration), + builder_create_requested: 0, + builder_session_id: null, + validator_name: deterministicName( + 'snapshot-validator', + parsed.credentialId, + credential.runtimeBuildId ?? parsed.buildGeneration + ), + validator_operation_id: operationId('validator', parsed.buildGeneration), + validator_create_requested: 0, + validator_session_id: null, + validator_wrapper_requested: 0, + validator_wrapper_command_id: null, + validator_control_id: null, + team_slug: null, + project_slug: null, + snapshot_baseline_json: null, + snapshot_source_session_id: null, + snapshot_id: null, + snapshot_candidate_id: null, + snapshot_requested: 0, + last_error: null, + created_at: timestamp, + updated_at: timestamp, + } satisfies typeof vercelSnapshotBuilds.$inferInsert; + + const previousResources = + current && current.credential_id === parsed.credentialId && this.hasBuildResources(current) + ? current + : undefined; + const handover = previousResources + ? { + ...previousResources, + credential_id: next.credential_id, + build_generation: next.build_generation, + next_attempt_at: null, + updated_at: timestamp, + } + : next; + + const applied = current + ? this.db + .update(vercelSnapshotBuilds) + .set(handover) + .where(this.ownerCondition(current)) + .returning({ organization_id: vercelSnapshotBuilds.organization_id }) + .get() + : this.db + .insert(vercelSnapshotBuilds) + .values(next) + .onConflictDoNothing({ target: vercelSnapshotBuilds.organization_id }) + .returning({ organization_id: vercelSnapshotBuilds.organization_id }) + .get(); + if (!applied) return; + + if (previousResources) { + try { + const config = await this.external(handover, () => + resolveByocVercelAccessConfig( + this.env, + ownerCredentialFetchInput(owner, parsed.credentialId) + ) + ); + await this.removeBuildResources(handover, this.createClient(config), previousResources); + if (!this.save(next)) return; + } catch (error) { + if (error instanceof SnapshotBuildOwnershipLostError) return; + if (error instanceof ByocCredentialMissingError) { + await this.erase(handover); + } else { + const cleanup = this.next(handover, { step: 'cleanup' }); + if (this.save(cleanup)) await this.arm(cleanup, now() + BUILD_ALARM_DELAY_MS); + } + throw error; + } + } + + await this.arm(next, timestamp); + } + + async cleanup(input: z.infer): Promise { + const parsed = CleanupInputSchema.parse(input); + const owner = ownerFromInput(parsed); + if (ownerToRowKey(owner) !== this.ctx.id.name) { + throw new Error('snapshot_owner_mismatch'); + } + + const ownership = ownershipFromInput(parsed); + const observed = this.load(); + if (observed && !sameOwnership(observed, ownership)) return; + + try { + const credential = await this.cleanupExternal(ownership, () => + fetchByocVercelCredential(this.env, ownerCredentialFetchInput(owner, parsed.credentialId)) + ); + if (credential.buildGeneration !== parsed.buildGeneration) return; + + const current = this.loadOwned(ownership); + if (!parsed.snapshotId && (!current || !this.hasBuildResources(current))) { + if (current) await this.erase(current); + return; + } + + const config = await this.cleanupExternal(ownership, () => + resolveByocVercelAccessConfig( + this.env, + ownerCredentialFetchInput(owner, parsed.credentialId) + ) + ); + const client = this.createClient(config); + const latest = this.loadOwned(ownership); + const explicitSnapshotIds = [ + parsed.snapshotId, + observed?.snapshot_id, + observed?.snapshot_candidate_id, + current?.snapshot_id, + current?.snapshot_candidate_id, + ].filter((snapshotId): snapshotId is string => Boolean(snapshotId)); + + await this.removeBuildResources( + ownership, + client, + latest ?? current ?? observed, + explicitSnapshotIds, + true + ); + + const remaining = this.loadOwned(ownership); + if (remaining) await this.erase(remaining); + } catch (error) { + if (error instanceof SnapshotBuildOwnershipLostError) return; + if (error instanceof ByocCredentialMissingError) { + const remaining = this.loadOwned(ownership); + if (remaining) await this.erase(remaining); + return; + } + throw error; + } + } + + async alarm(): Promise { + const state = this.load(); + if (!state || state.next_attempt_at === null) return; + const ownership = ownershipFromRow(state); + const owner = ownerFromRow(state); + if (state.next_attempt_at > now()) { + await this.arm(ownership, state.next_attempt_at); + return; + } + + let credential: ByocVercelCredential; + try { + credential = await this.external(ownership, () => + fetchByocVercelCredential(this.env, ownerCredentialFetchInput(owner, state.credential_id)) + ); + } catch (error) { + if (error instanceof SnapshotBuildOwnershipLostError) return; + await this.handleFailure(ownership, error); + return; + } + + try { + if (credential.buildGeneration !== state.build_generation) { + await this.abandon(ownership); + return; + } + if (state.step === 'project_failure' || state.step === 'cleanup') { + if (state.step === 'project_failure') { + await this.external(ownership, () => + projectByocVercelStatus(this.env, { + ...this.projection(state), + setupStatus: 'failed', + setupError: state.last_error, + setupStep: null, + runtimeSnapshotId: null, + setupCompletedAt: null, + }) + ); + } + await this.abandon(ownership); + return; + } + + const config = await this.external(ownership, () => + resolveByocVercelAccessConfig( + this.env, + ownerCredentialFetchInput(owner, state.credential_id) + ) + ); + const client = this.createClient(config); + const next = await this.runStep(state, client, config); + if (!next || !this.save(next)) return; + + const terminalConfirmed = state.step === 'confirm_terminal'; + const identity = terminalConfirmed + ? await this.external(ownershipFromRow(next), async () => { + try { + return await getVercelRuntimeIdentity(this.env); + } catch { + return undefined; + } + }) + : undefined; + const previousSnapshotId = credential.runtimeSnapshotId; + const projected = await this.external(ownershipFromRow(next), () => + projectByocVercelStatus(this.env, this.projection(next, terminalConfirmed, identity)) + ); + if (!projected) { + await this.abandon(ownershipFromRow(next), client); + return; + } + + if ( + terminalConfirmed && + previousSnapshotId && + next.snapshot_id && + previousSnapshotId !== next.snapshot_id + ) { + try { + await this.external(ownershipFromRow(next), () => + client.deleteSnapshot(previousSnapshotId) + ); + } catch (error) { + if (error instanceof SnapshotBuildOwnershipLostError) throw error; + if ( + !(error instanceof VercelSandboxRestError) || + (error.status !== 404 && error.status !== 410) + ) { + logger + .withFields({ + logTag: 'byoc_vercel_snapshot_build', + credentialId: next.credential_id, + buildGeneration: next.build_generation, + snapshotId: previousSnapshotId, + diagnosticEvent: 'superseded_snapshot_cleanup_failed', + failureCode: safeError(error), + }) + .warn('Could not remove superseded Vercel snapshot'); + } + } + } + + if (terminalConfirmed && next.step === 'confirm_terminal') { + await this.erase(ownershipFromRow(next)); + } else { + if (!this.save({ ...next, retry_count: 0 })) return; + await this.arm(ownershipFromRow(next), now() + BUILD_ALARM_DELAY_MS); + } + } catch (error) { + if (error instanceof SnapshotBuildOwnershipLostError) return; + await this.handleFailure(ownership, error); + } + } + + private async runStep( + state: BuildRow, + client: VercelSandboxRestClient, + config: VercelSandboxRuntimeConfig + ): Promise { + const owner = ownerFromRow(state); + switch (BuildStepSchema.parse(state.step)) { + case 'validating_access': { + if (config.scope !== 'project' && !state.team_slug) { + const team = await this.external(state, () => client.inspectTeam()); + return this.next(state, { team_slug: team.slug }); + } + const project = await this.external(state, () => client.inspectProject()); + return this.next(state, { + step: 'create_builder', + project_slug: project.name, + last_error: null, + }); + } + case 'create_builder': { + const input = buildInput(state, 'runtime'); + let created; + if (state.builder_create_requested === 1) { + const intent = this.next(state, { builder_create_requested: 2 }); + if (!this.save(intent)) throw new SnapshotBuildOwnershipLostError(); + created = await this.createManagedSandbox(intent, client, input); + } else { + const { timeoutMs: _timeoutMs, ...inspectInput } = input; + const existing = await this.external(state, () => client.inspectByName(inspectInput)); + if (existing) { + created = existing; + } else if (state.builder_create_requested === 2) { + throw new VercelSandboxRestError('request_failed', 'inspect'); + } else { + const prepared = this.next(state, { builder_create_requested: 1 }); + if (!this.save(prepared)) throw new SnapshotBuildOwnershipLostError(); + return prepared; + } + } + return this.next(state, { + step: 'install_system_dependencies', + builder_session_id: created.session.id, + last_error: null, + }); + } + case 'install_system_dependencies': { + const builderSessionId = requiredBuildField( + state.builder_session_id, + 'builder_session_missing' + ); + await this.runCommand(state, client, builderSessionId, 'bash', [ + '-lc', + VERCEL_BASE_SYSTEM_SETUP, + ]); + return this.next(state, { step: 'install_node_dependencies' }); + } + case 'install_node_dependencies': { + const builderSessionId = requiredBuildField( + state.builder_session_id, + 'builder_session_missing' + ); + await this.runCommand(state, client, builderSessionId, 'bash', [ + '-lc', + `${VERCEL_BASE_NODE_SETUP}; sudo npm install -g @kilocode/cli@${PINNED_KILO_VERSION}; test "$(bun --version)" = ${JSON.stringify(PINNED_BUN_VERSION)}; case "$(node --version)" in v24.*) ;; *) exit 1 ;; esac; git --version >/dev/null; kilo --version >/dev/null`, + ]); + return this.next(state, { step: 'upload_runtime_artifacts' }); + } + case 'upload_runtime_artifacts': { + const builderSessionId = requiredBuildField( + state.builder_session_id, + 'builder_session_missing' + ); + const artifacts = await this.external(state, () => getVercelRuntimeArtifacts()); + const controlWrapper = artifacts.find( + artifact => artifact.path === 'usr/local/bin/kilocode-control-plane-wrapper.js' + ); + const supervisor = artifacts.find( + artifact => artifact.path === 'usr/local/bin/kilocode-control-plane-supervisor.sh' + ); + if (!controlWrapper || !supervisor) throw new Error('runtime_artifact_missing'); + const manifest = JSON.stringify({ + runtimeBuildId: state.runtime_build_id, + wrapperVersion: WRAPPER_VERSION, + runtime: 'node24', + bunVersion: PINNED_BUN_VERSION, + controlWrapperSha256: controlWrapper.sha256, + controlPlaneSupervisorSha256: supervisor.sha256, + }); + await this.external(state, () => + client.writeFiles(builderSessionId, '/tmp', [ + { path: 'kilocode-control-plane-wrapper.js', content: controlWrapper.bytes }, + { path: 'kilocode-control-plane-supervisor.sh', content: supervisor.bytes }, + { path: 'kilo-runtime-manifest.json', content: manifest }, + ]) + ); + return this.next(state, { step: 'verify_runtime_artifacts' }); + } + case 'verify_runtime_artifacts': { + const builderSessionId = requiredBuildField( + state.builder_session_id, + 'builder_session_missing' + ); + await this.runCommand(state, client, builderSessionId, 'bash', [ + '-lc', + `set -euo pipefail; sudo install -m 0755 /tmp/kilocode-control-plane-wrapper.js /usr/local/bin/kilocode-control-plane-wrapper.js; sudo install -m 0755 /tmp/kilocode-control-plane-supervisor.sh /usr/local/bin/kilocode-control-plane-supervisor.sh; sudo install -m 0644 /tmp/kilo-runtime-manifest.json /usr/local/share/kilo/runtime-manifest.json; test -f /usr/local/bin/kilocode-control-plane-wrapper.js; test "$(bun --version)" = ${JSON.stringify(PINNED_BUN_VERSION)}; test "$(node --version | cut -c1-3)" = "v24"; control_hash="$(sha256sum /usr/local/bin/kilocode-control-plane-wrapper.js | cut -d' ' -f1)"; jq -e --arg build ${JSON.stringify(state.runtime_build_id)} --arg bun ${JSON.stringify(PINNED_BUN_VERSION)} --arg control "$control_hash" '.runtimeBuildId == $build and .bunVersion == $bun and .controlWrapperSha256 == $control' /usr/local/share/kilo/runtime-manifest.json >/dev/null`, + ]); + return this.next(state, { + step: 'snapshot_builder', + snapshot_source_session_id: state.builder_session_id, + snapshot_baseline_json: null, + snapshot_requested: 0, + }); + } + case 'snapshot_builder': + return this.advanceSnapshot(state, client); + case 'create_validator': { + if (!state.snapshot_id) throw new Error('snapshot_result_missing'); + const input = validatorInput(state); + let created; + if (state.validator_create_requested === 1) { + const intent = this.next(state, { validator_create_requested: 2 }); + if (!this.save(intent)) throw new SnapshotBuildOwnershipLostError(); + created = await this.createManagedSandbox(intent, client, input); + } else { + const { timeoutMs: _timeoutMs, ...inspectInput } = input; + const existing = await this.external(state, () => client.inspectByName(inspectInput)); + if (existing) { + created = existing; + } else if (state.validator_create_requested === 2) { + throw new VercelSandboxRestError('request_failed', 'inspect'); + } else { + const prepared = this.next(state, { validator_create_requested: 1 }); + if (!this.save(prepared)) throw new SnapshotBuildOwnershipLostError(); + return prepared; + } + } + return this.next(state, { + step: 'launch_validator_wrapper', + validator_session_id: created.session.id, + }); + } + case 'launch_validator_wrapper': { + const validatorSessionId = requiredBuildField( + state.validator_session_id, + 'validator_session_missing' + ); + const validatorControlId = state.validator_control_id ?? controlId(state.build_generation); + if (state.validator_wrapper_command_id) { + return this.next(state, { + step: 'verify_validator_call_home', + validator_control_id: validatorControlId, + }); + } + if (state.validator_wrapper_requested === 1) { + const commands = await this.external(state, () => + client.listCommands(validatorSessionId) + ); + const matches = commands.filter(command => + command.args.some(argument => + argument.includes(`byoc-call-home:${state.build_generation}`) + ) + ); + if (matches.length > 1) throw new Error('validator_call_home_ambiguous'); + const command = matches[0]; + if (!command) throw new VercelSandboxRestError('request_failed', 'list-commands'); + return this.next(state, { + step: 'verify_validator_call_home', + validator_control_id: validatorControlId, + validator_wrapper_command_id: command.id, + }); + } + + if (!this.env.WORKER_URL) throw new Error('provider_configuration_failed'); + const { session } = await this.external(state, () => + client.getSession(validatorSessionId, state.validator_name) + ); + if (session.status === 'pending') { + throw new VercelSandboxRestError('request_failed', 'get-session'); + } + if (session.status !== 'running') throw new Error('validator_not_running'); + const credential = generateSandboxCredential(); + const credentialHash = await this.external(state, () => hashSandboxCredential(credential)); + const allocationId = state.build_generation; + const signingSecret = await this.external(state, () => + resolveSecret(this.env.NEXTAUTH_SECRET) + ); + if (!signingSecret) throw new Error('provider_configuration_failed'); + const launchCredential = mintSandboxLaunchCredential( + { sandboxId: validatorControlId, allocationId, credential }, + signingSecret + ); + const providerRef = encodeVercelProviderRef({ + sandboxName: state.validator_name, + sessionId: validatorSessionId, + }); + const prepared = this.next(state, { validator_control_id: validatorControlId }); + if (!this.save(prepared)) throw new SnapshotBuildOwnershipLostError(); + await this.external(prepared, () => + withDORetry( + () => getSandboxControlStub(this.env, validatorControlId), + stub => + stub.initializeSnapshotValidator({ + build: { + ...ownerToProjection(owner), + credentialId: state.credential_id, + generation: state.build_generation, + }, + allocation: { + providerRef, + locator: { + teamId: config.teamId, + projectId: config.projectId, + snapshotId: requiredBuildField(state.snapshot_id, 'snapshot_result_missing'), + runtimeBuildId: state.runtime_build_id, + runtime: 'node24', + }, + createdAt: session.requestedAt, + expiresAt: (session.startedAt ?? session.requestedAt) + session.timeout, + }, + credentialHash, + }), + 'seedByocValidatorControl' + ) + ); + const intent = this.next(prepared, { validator_wrapper_requested: 1 }); + if (!this.save(intent)) throw new SnapshotBuildOwnershipLostError(); + const command = await this.external(intent, () => + client.executeCommand(validatorSessionId, { + command: 'sh', + args: [ + '-lc', + `exec ${CONTROL_SUPERVISOR_PATH} # byoc-call-home:${state.build_generation}`, + ], + cwd: '/', + env: { + ...buildControlWrapperLaunchEnv({ + provider: 'vercel', + workerUrl: this.env.WORKER_URL, + sandboxId: validatorControlId, + credential: launchCredential, + }), + PROVIDER_INSTANCE_ID: providerRef, + [CONTROL_PLANE_ALLOCATION_ID_ENV]: allocationId, + }, + sudo: false, + wait: false, + }) + ); + return this.next(intent, { + step: 'verify_validator_call_home', + validator_wrapper_command_id: command.id, + }); + } + case 'verify_validator_call_home': { + const validatorSessionId = requiredBuildField( + state.validator_session_id, + 'validator_session_missing' + ); + const validatorCommandId = requiredBuildField( + state.validator_wrapper_command_id, + 'validator_command_missing' + ); + const validatorControlId = requiredBuildField( + state.validator_control_id, + 'validator_control_missing' + ); + const command = await this.external(state, () => + client.getCommand(validatorSessionId, validatorCommandId) + ); + if (command.exitCode !== null) { + logger + .withFields({ + logTag: 'byoc_vercel_snapshot_build', + ownerId: owner.type === 'org' ? owner.organizationId : owner.userId, + ownerType: owner.type, + credentialId: state.credential_id, + buildGeneration: state.build_generation, + step: state.step, + diagnosticEvent: 'validator_command_exited', + expectedExitCode: 'null', + observedExitCode: command.exitCode, + }) + .warn('BYOC Vercel snapshot build check failed'); + if (command.exitCode !== 0) throw new Error('validator_call_home_failed'); + throw new Error('validator_call_home_ended'); + } + const status = await this.external(state, () => + withDORetry( + () => getSandboxControlStub(this.env, validatorControlId), + stub => stub.getSnapshotValidatorStatus(), + 'verifyByocValidatorCallHome' + ) + ); + if (status.physical !== 'running') { + logger + .withFields({ + logTag: 'byoc_vercel_snapshot_build', + ownerId: owner.type === 'org' ? owner.organizationId : owner.userId, + ownerType: owner.type, + credentialId: state.credential_id, + buildGeneration: state.build_generation, + step: state.step, + diagnosticEvent: 'validator_physical_not_running', + expectedPhysical: 'running', + observedPhysical: status.physical, + observedConnection: status.connection, + }) + .warn('BYOC Vercel snapshot build check failed'); + throw new Error('validator_call_home_failed'); + } + return status.connection === 'ready' + ? this.next(state, { step: 'stop_validator' }) + : this.next(state, { step: 'verify_validator_call_home' }); + } + case 'stop_validator': { + const validatorSessionId = requiredBuildField( + state.validator_session_id, + 'validator_session_missing' + ); + await this.stopBuildSession(state, client, validatorSessionId, state.validator_name, false); + await this.confirmValidatorStopped(state, state, validatorSessionId, false); + return this.next(state, { step: 'confirm_terminal' }); + } + case 'confirm_terminal': { + const validatorSessionId = requiredBuildField( + state.validator_session_id, + 'validator_session_missing' + ); + try { + const observed = await this.external(state, () => + client.getSession(validatorSessionId, state.validator_name) + ); + if ( + observed.session.status === 'stopped' || + observed.session.status === 'failed' || + observed.session.status === 'aborted' + ) { + return this.next(state, { step: 'confirm_terminal', last_error: null }); + } + } catch (error) { + if ( + error instanceof VercelSandboxRestError && + (error.status === 404 || error.status === 410) + ) { + return this.next(state, { step: 'confirm_terminal', last_error: null }); + } + throw error; + } + throw new VercelSandboxRestError('request_failed', 'get-session'); + } + } + } + + private async createManagedSandbox( + owner: BuildRow, + client: VercelSandboxRestClient, + input: CreateSandboxInput + ): Promise { + if (!this.loadOwned(owner)) throw new SnapshotBuildOwnershipLostError(); + + let created: VercelSandboxCreateEnvelope; + try { + created = await client.createSandbox(input); + } catch (error) { + // Only a rejected POST proves no sandbox was created. Transport and parse failures do not. + if ( + error instanceof VercelSandboxRestError && + error.kind === 'request_failed' && + error.status !== undefined && + error.status >= 400 && + error.status < 500 && + error.status !== 408 && + error.status !== 409 + ) { + const current = this.loadOwned(owner); + if (current) { + this.save({ + ...current, + ...(owner.step === 'create_builder' + ? { builder_create_requested: 0 } + : { validator_create_requested: 0 }), + }); + } + } + throw error; + } + if (this.loadOwned(owner)) return created; + + await this.removeDetachedSandbox(owner, client, input, created); + throw new SnapshotBuildOwnershipLostError(); + } + + private async advanceSnapshot( + state: BuildRow, + client: VercelSandboxRestClient + ): Promise { + const sourceSessionId = state.snapshot_source_session_id; + if (!sourceSessionId) throw new Error('snapshot_source_missing'); + const baseline = snapshotBaseline(state.snapshot_baseline_json); + if (!baseline) { + const snapshots = await this.external(state, () => client.listSnapshots()); + return this.next(state, { + snapshot_baseline_json: JSON.stringify(snapshots.map(snapshot => snapshot.id)), + }); + } + + if (state.snapshot_candidate_id) { + const snapshot = await this.external(state, () => + client.inspectSnapshot( + requiredBuildField(state.snapshot_candidate_id, 'snapshot_candidate_missing') + ) + ); + if (!snapshotMatches(snapshot, sourceSessionId, baseline)) { + throw new Error('snapshot_result_ambiguous'); + } + return this.next(state, { + snapshot_id: snapshot.id, + snapshot_candidate_id: null, + snapshot_requested: 2, + step: 'create_validator', + }); + } + + if (state.snapshot_requested === 1) { + const requested = this.next(state, { snapshot_requested: 2 }); + if (!this.save(requested)) throw new SnapshotBuildOwnershipLostError(); + const snapshot = await client.createSnapshot(sourceSessionId, SNAPSHOT_EXPIRATION_MS); + if (!this.loadOwned(requested)) { + await this.removeDetachedSnapshot(requested, client, snapshot.id); + throw new SnapshotBuildOwnershipLostError(); + } + return this.next(requested, { snapshot_id: snapshot.id, step: 'create_validator' }); + } + + const snapshots = await this.external(state, () => client.listSnapshots()); + const candidates = snapshots.filter(snapshot => + snapshotMatches(snapshot, sourceSessionId, baseline) + ); + if (candidates.length > 1) throw new Error('snapshot_result_ambiguous'); + if (candidates.length === 1) { + const [candidate] = candidates; + if (!candidate) throw new Error('snapshot_result_ambiguous'); + return this.next(state, { snapshot_candidate_id: candidate.id }); + } + if (state.snapshot_requested === 0) { + const requested = this.next(state, { snapshot_requested: 1 }); + if (!this.save(requested)) throw new SnapshotBuildOwnershipLostError(); + return requested; + } + throw new VercelSandboxRestError('request_failed', 'list-snapshots'); + } + + private async runCommand( + owner: BuildOwnership, + client: VercelSandboxRestClient, + sessionId: string, + command: string, + args: string[], + timeoutMs = BUILDER_TIMEOUT_MS + ): Promise { + const result = await this.external(owner, () => + client.executeCommand(sessionId, { + command, + args, + cwd: '/', + env: {}, + sudo: false, + wait: true, + timeoutMs, + }) + ); + if (result.finished.exitCode !== 0) throw new Error('setup_command_failed'); + return result.finished; + } + + private next(state: BuildRow, changes: Partial): BuildRow { + return { ...state, ...changes, next_attempt_at: now(), updated_at: now() }; + } + + private projection( + state: BuildRow, + terminalConfirmed = false, + identity?: { wrapperVersion: string; releasedAt: string; digest: string } + ) { + const owner = ownerFromRow(state); + const ready = + terminalConfirmed && state.step === 'confirm_terminal' && state.snapshot_id !== null; + return { + ...ownerToProjection(owner), + credentialId: state.credential_id, + buildGeneration: state.build_generation, + setupStatus: ready ? ('ready' as const) : ('building' as const), + setupStep: + ready || state.step === 'project_failure' || state.step === 'cleanup' + ? null + : BuildStepSchema.parse(state.step), + setupError: null, + teamSlug: state.team_slug, + projectSlug: state.project_slug, + runtimeBuildId: state.runtime_build_id, + runtimeSnapshotId: ready ? state.snapshot_id : null, + setupStartedAt: iso(state.created_at), + setupCompletedAt: ready ? iso(state.updated_at) : null, + runtimeWrapperVersion: ready ? (identity?.wrapperVersion ?? null) : null, + runtimeReleasedAt: ready ? (identity?.releasedAt ?? null) : null, + runtimeDigest: ready ? (identity?.digest ?? null) : null, + }; + } + + private async handleFailure(owner: BuildOwnership, error: unknown): Promise { + const state = this.loadOwned(owner); + if (!state) return; + + if (error instanceof ByocCredentialMissingError) { + await this.erase(state); + return; + } + + const terminal = state.step === 'project_failure' || state.step === 'cleanup'; + const attempt = state.retry_count + 1; + if (terminal || (isRetryable(error) && attempt <= MAX_RETRIES)) { + logSnapshotBuildFailure(state, error, 'retry'); + const retrying = { + ...state, + retry_count: Math.min(attempt, MAX_RETRIES + 1), + next_attempt_at: now() + (RETRY_DELAY_MS[attempt - 1] ?? MAX_RETRY_DELAY_MS), + last_error: terminal ? state.last_error : safeError(error), + updated_at: now(), + } satisfies BuildRow; + if (!this.save(retrying)) return; + await this.arm(retrying, retrying.next_attempt_at); + return; + } + + logSnapshotBuildFailure(state, error, 'terminal'); + const failed = this.next(state, { + step: 'project_failure', + retry_count: 0, + last_error: safeError(error), + }); + if (!this.save(failed)) return; + await this.arm(failed, now() + BUILD_ALARM_DELAY_MS); + } + + private createClient( + config: Awaited> + ): VercelSandboxRestClient { + return new VercelSandboxRestClient({ + accessToken: config.accessToken, + teamId: config.teamId, + scope: config.scope, + projectId: config.projectId, + fetch, + }); + } + + private hasBuildResources(state: BuildRow): boolean { + return Boolean( + state.builder_session_id || + state.validator_session_id || + state.builder_create_requested > 0 || + state.validator_create_requested > 0 || + state.snapshot_id || + state.snapshot_candidate_id || + (state.snapshot_requested > 0 && + state.snapshot_source_session_id && + state.snapshot_baseline_json) + ); + } + + private async abandon(owner: BuildOwnership, client?: VercelSandboxRestClient): Promise { + let state = this.loadOwned(owner); + if (!state) return; + if (state.step !== 'cleanup') { + state = this.next(state, { step: 'cleanup' }); + if (!this.save(state)) return; + await this.arm(state, now() + BUILD_ALARM_DELAY_MS); + } + const cleanup = state; + + if (this.hasBuildResources(cleanup)) { + let credential: ByocVercelCredential; + try { + credential = await this.external(cleanup, () => + fetchByocVercelCredential( + this.env, + ownerCredentialFetchInput(ownerFromRow(cleanup), cleanup.credential_id) + ) + ); + } catch (error) { + if (error instanceof ByocCredentialMissingError) { + await this.erase(cleanup); + return; + } + throw error; + } + + if (!client || credential.buildGeneration !== cleanup.build_generation) { + const config = await this.external(cleanup, () => + resolveByocVercelAccessConfig( + this.env, + ownerCredentialFetchInput(ownerFromRow(cleanup), cleanup.credential_id) + ) + ); + client = this.createClient(config); + } + + await this.removeBuildResources(cleanup, client, cleanup); + } + + await this.erase(cleanup); + } + + private async removeBuildResources( + owner: BuildOwnership, + client: VercelSandboxRestClient, + state?: BuildRow, + additionalSnapshotIds: string[] = [], + allowMissingOwner = false + ): Promise { + if (state) { + await this.removeManagedSession(owner, client, state, 'validator', allowMissingOwner); + if (!state.snapshot_id) { + await this.removeManagedSession(owner, client, state, 'builder', allowMissingOwner); + } + } + + await this.removeSnapshotResources( + owner, + client, + state, + additionalSnapshotIds, + allowMissingOwner + ); + } + + private async removeManagedSession( + owner: BuildOwnership, + client: VercelSandboxRestClient, + state: BuildRow, + kind: 'builder' | 'validator', + allowMissingOwner: boolean + ): Promise { + const sessionId = kind === 'builder' ? state.builder_session_id : state.validator_session_id; + const sandboxName = kind === 'builder' ? state.builder_name : state.validator_name; + + if (sessionId) { + await this.stopBuildSession(owner, client, sessionId, sandboxName, allowMissingOwner); + if (kind === 'validator') { + await this.confirmValidatorStopped(owner, state, sessionId, allowMissingOwner); + } + return; + } + + const requested = + kind === 'builder' ? state.builder_create_requested : state.validator_create_requested; + if (requested === 0) return; + + const input = kind === 'builder' ? buildInput(state, 'runtime') : validatorInput(state); + const { timeoutMs: _timeoutMs, ...inspectInput } = input; + const existing = allowMissingOwner + ? await this.cleanupExternal(owner, () => client.inspectByName(inspectInput)) + : await this.external(owner, () => client.inspectByName(inspectInput)); + + if (!existing) { + if (requested > 1) throw new VercelSandboxRestError('request_failed', 'inspect'); + return; + } + + await this.stopBuildSession(owner, client, existing.session.id, sandboxName, allowMissingOwner); + if (kind === 'validator') { + await this.confirmValidatorStopped(owner, state, existing.session.id, allowMissingOwner); + } + } + + private async confirmValidatorStopped( + owner: BuildOwnership, + state: BuildRow, + sessionId: string, + allowMissingOwner: boolean + ): Promise { + const validatorControlId = state.validator_control_id; + if (!validatorControlId) return; + const providerRef = encodeVercelProviderRef({ sandboxName: state.validator_name, sessionId }); + const confirm = () => + withDORetry( + () => getSandboxControlStub(this.env, validatorControlId), + stub => stub.confirmSnapshotValidatorStopped(providerRef), + 'confirmByocValidatorStopped' + ); + if (allowMissingOwner) { + await this.cleanupExternal(owner, confirm); + } else { + await this.external(owner, confirm); + } + } + + private async stopBuildSession( + owner: BuildOwnership, + client: VercelSandboxRestClient, + sessionId: string, + sandboxName: string, + allowMissingOwner: boolean + ): Promise { + try { + const session = allowMissingOwner + ? await this.cleanupExternal(owner, () => client.stopSession(sessionId, sandboxName)) + : await this.external(owner, () => client.stopSession(sessionId, sandboxName)); + + if ( + session.status !== 'stopped' && + session.status !== 'failed' && + session.status !== 'aborted' + ) { + throw new VercelSandboxRestError('request_failed', 'stop-session'); + } + } catch (error) { + if ( + !(error instanceof VercelSandboxRestError) || + (error.status !== 404 && error.status !== 410) + ) { + throw error; + } + + if (allowMissingOwner) { + const current = this.load(); + if (current && !sameOwnership(current, owner)) { + throw new SnapshotBuildOwnershipLostError(); + } + } else if (!this.loadOwned(owner)) { + throw new SnapshotBuildOwnershipLostError(); + } + } + } + + private async removeSnapshotResources( + owner: BuildOwnership, + client: VercelSandboxRestClient, + state?: BuildRow, + additionalSnapshotIds: string[] = [], + allowMissingOwner = false + ): Promise { + const snapshotIds = new Set(additionalSnapshotIds); + if (state?.snapshot_id) snapshotIds.add(state.snapshot_id); + if (state?.snapshot_candidate_id) snapshotIds.add(state.snapshot_candidate_id); + + if ( + state && + state.snapshot_requested > 0 && + !state.snapshot_id && + state.snapshot_source_session_id + ) { + const baseline = snapshotBaseline(state.snapshot_baseline_json); + if (baseline) { + const observed = allowMissingOwner + ? await this.cleanupExternal(owner, () => client.listSnapshots()) + : await this.external(owner, () => client.listSnapshots()); + for (const snapshot of observed) { + if (snapshotMatches(snapshot, state.snapshot_source_session_id, baseline)) { + snapshotIds.add(snapshot.id); + } + } + } + } + + for (const snapshotId of snapshotIds) { + try { + if (allowMissingOwner) { + await this.cleanupExternal(owner, () => client.deleteSnapshot(snapshotId)); + } else { + await this.external(owner, () => client.deleteSnapshot(snapshotId)); + } + } catch (error) { + if (!(error instanceof VercelSandboxRestError) || error.status !== 404) throw error; + if (allowMissingOwner) { + const current = this.load(); + if (current && !sameOwnership(current, owner)) { + throw new SnapshotBuildOwnershipLostError(); + } + } else if (!this.loadOwned(owner)) { + throw new SnapshotBuildOwnershipLostError(); + } + } + } + } + + private async removeDetachedSandbox( + owner: BuildOwnership, + client: VercelSandboxRestClient, + input: CreateSandboxInput, + created: VercelSandboxCreateEnvelope + ): Promise { + const expectedSnapshotId = + input.source?.type === 'snapshot' ? input.source.snapshotId : input.snapshotId; + + if ( + created.sandbox.name !== input.name || + created.sandbox.persistent || + created.sandbox.currentSessionId !== created.session.id || + created.sandbox.tags[VERCEL_CLOUD_AGENT_RESOURCE_TAG] !== + VERCEL_CLOUD_AGENT_RESOURCE_TAG_VALUE || + created.sandbox.tags[VERCEL_CLOUD_AGENT_CREATE_OPERATION_TAG] !== input.operationId || + created.sandbox.tags[VERCEL_CLOUD_AGENT_RUNTIME_BUILD_TAG] !== input.runtimeBuildId || + created.session.sourceSandboxName !== input.name || + created.session.sourceSnapshotId !== expectedSnapshotId || + created.session.runtime !== input.runtime || + created.runtime.sandboxName !== input.name || + created.runtime.sessionId !== created.session.id + ) { + throw new VercelSandboxRestError('correlation_mismatch', 'create'); + } + + try { + await fetchByocVercelCredential( + this.env, + ownerCredentialFetchInput(ownerFromRow(owner), owner.credential_id) + ); + } catch (error) { + if (error instanceof ByocCredentialMissingError) return; + throw error; + } + + const current = this.load(); + if (current && current.credential_id !== owner.credential_id) return; + if (current && sameOwnership(current, owner)) return; + + if (current) { + const builderMatches = + current.runtime_build_id === input.runtimeBuildId && + current.builder_name === input.name && + current.builder_operation_id === input.operationId && + current.builder_create_requested > 0; + const validatorMatches = + current.runtime_build_id === input.runtimeBuildId && + current.validator_name === input.name && + current.validator_operation_id === input.operationId && + current.validator_create_requested > 0; + + if ( + (current.builder_session_id === created.session.id && !builderMatches) || + (current.validator_session_id === created.session.id && !validatorMatches) + ) { + return; + } + + if (builderMatches && current.builder_session_id === null) { + if (!this.save({ ...current, builder_session_id: created.session.id, updated_at: now() })) { + return; + } + } else if (validatorMatches && current.validator_session_id === null) { + if ( + !this.save({ ...current, validator_session_id: created.session.id, updated_at: now() }) + ) { + return; + } + } + } + + try { + const stopped = await client.stopSession(created.session.id, input.name); + if ( + stopped.status !== 'stopped' && + stopped.status !== 'failed' && + stopped.status !== 'aborted' + ) { + throw new VercelSandboxRestError('request_failed', 'stop-session'); + } + } catch (error) { + if ( + !(error instanceof VercelSandboxRestError) || + (error.status !== 404 && error.status !== 410) + ) { + throw error; + } + } + } + + private async removeDetachedSnapshot( + owner: BuildOwnership, + client: VercelSandboxRestClient, + snapshotId: string + ): Promise { + try { + await fetchByocVercelCredential( + this.env, + ownerCredentialFetchInput(ownerFromRow(owner), owner.credential_id) + ); + } catch (error) { + if (error instanceof ByocCredentialMissingError) return; + throw error; + } + + const current = this.load(); + if (current && current.credential_id !== owner.credential_id) return; + + try { + await client.deleteSnapshot(snapshotId); + } catch (error) { + if (!(error instanceof VercelSandboxRestError) || error.status !== 404) throw error; + } + } + + private async external(owner: BuildOwnership, action: () => Promise): Promise { + if (!this.loadOwned(owner)) throw new SnapshotBuildOwnershipLostError(); + const result = await action(); + if (!this.loadOwned(owner)) throw new SnapshotBuildOwnershipLostError(); + return result; + } + + private async cleanupExternal(owner: BuildOwnership, action: () => Promise): Promise { + const initial = this.load(); + if (initial && !sameOwnership(initial, owner)) throw new SnapshotBuildOwnershipLostError(); + const result = await action(); + const current = this.load(); + if (current && !sameOwnership(current, owner)) throw new SnapshotBuildOwnershipLostError(); + return result; + } + + private ownerCondition(owner: BuildOwnership) { + const ownerDescriptor = ownerFromRow(owner); + return and( + eq(vercelSnapshotBuilds.organization_id, ownerToRowKey(ownerDescriptor)), + eq(vercelSnapshotBuilds.owner_type, ownerDescriptor.type), + eq(vercelSnapshotBuilds.credential_id, owner.credential_id), + eq(vercelSnapshotBuilds.build_generation, owner.build_generation) + ); + } + + private load(): BuildRow | undefined { + return this.db + .select() + .from(vercelSnapshotBuilds) + .where(eq(vercelSnapshotBuilds.organization_id, this.ctx.id.name ?? '')) + .get(); + } + + private loadOwned(owner: BuildOwnership): BuildRow | undefined { + if (ownerToRowKey(ownerFromRow(owner)) !== this.ctx.id.name) return undefined; + return this.db.select().from(vercelSnapshotBuilds).where(this.ownerCondition(owner)).get(); + } + + private save(row: BuildRow): boolean { + return Boolean( + this.db + .update(vercelSnapshotBuilds) + .set(row) + .where(this.ownerCondition(row)) + .returning({ organization_id: vercelSnapshotBuilds.organization_id }) + .get() + ); + } + + private async arm(owner: BuildOwnership, timestamp: number): Promise { + if (!this.loadOwned(owner)) return false; + await this.ctx.storage.setAlarm(timestamp); + return true; + } + + private async erase(owner: BuildOwnership): Promise { + const erased = this.db + .delete(vercelSnapshotBuilds) + .where(this.ownerCondition(owner)) + .returning({ organization_id: vercelSnapshotBuilds.organization_id }) + .get(); + if (!erased) return false; + await this.ctx.storage.deleteAlarm(); + return true; + } +} diff --git a/services/cloud-agent-next/src/persistence/session-metadata.ts b/services/cloud-agent-next/src/persistence/session-metadata.ts index c6e42acd147..fea84c574e3 100644 --- a/services/cloud-agent-next/src/persistence/session-metadata.ts +++ b/services/cloud-agent-next/src/persistence/session-metadata.ts @@ -26,6 +26,11 @@ import { isControlSession } from '../session-identity.js'; import { SHARED_SANDBOX_FAILOVER_SUFFIX } from '../shared-sandbox-route.js'; import { MESSAGE_ID_FORMAT_DESCRIPTION, MESSAGE_ID_PATTERN } from '../session/message-id.js'; import { agentSandboxProviderSchema, type AgentSandboxProvider } from '../types.js'; +import { + bindingFromLegacyProvider, + SandboxProviderBindingSchema, + type SandboxProviderBinding, +} from '../sandbox-provider-binding.js'; import { AttachmentsSchema, branchNameSchema, @@ -298,6 +303,7 @@ const MetadataWorkspaceSchema = egressPolicySchema sandboxKind: sandboxKindSchema.optional(), sandboxProvider: SandboxProviderSchema.optional(), sandboxAllocation: sandboxAllocationSchema.optional(), + sandboxProviderBinding: SandboxProviderBindingSchema.optional(), providerRuntime: ProviderRuntimeSchema.optional(), worktreeId: cloudAgentWorktreeIdSchema.optional(), workspacePath: z.string().optional(), @@ -396,6 +402,13 @@ const MetadataWorkspaceSchema = egressPolicySchema workspace.providerRuntime === undefined || workspace.providerRuntime.provider === workspace.sandboxProvider, 'Provider runtime must match the workspace sandbox provider' + ) + .refine( + workspace => + workspace.sandboxProviderBinding === undefined || + workspace.sandboxProvider === undefined || + workspace.sandboxProviderBinding.kind === workspace.sandboxProvider, + 'Provider binding must match the workspace sandbox provider' ); const MetadataDevContainerSchema = z @@ -476,6 +489,7 @@ export const CurrentSessionMetadataSchema = z export type SessionMetadata = z.infer; export type CredentialContainment = z.infer; +export type { SandboxProviderBinding }; export function getControlPlaneCredentialContainment( sessionId: string, @@ -531,7 +545,14 @@ export function requiresContainmentSandbox(metadata: SessionMetadata): boolean { } export function getSandboxProvider(metadata: SessionMetadata): AgentSandboxProvider { - return metadata.workspace?.sandboxProvider ?? 'cloudflare'; + return getSandboxProviderBinding(metadata).kind; +} + +export function getSandboxProviderBinding(metadata: SessionMetadata): SandboxProviderBinding { + return ( + metadata.workspace?.sandboxProviderBinding ?? + bindingFromLegacyProvider(metadata.workspace?.sandboxProvider ?? 'cloudflare') + ); } type LegacySessionMetadata = z.output; diff --git a/services/cloud-agent-next/src/router/handlers/session-worktree.test.ts b/services/cloud-agent-next/src/router/handlers/session-worktree.test.ts index 76b84a94578..4c2d4431a61 100644 --- a/services/cloud-agent-next/src/router/handlers/session-worktree.test.ts +++ b/services/cloud-agent-next/src/router/handlers/session-worktree.test.ts @@ -106,6 +106,14 @@ const LEDGER_ROW_ID = 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee'; const SHARED_SANDBOX_ID = 'usr-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; const INTERNAL_SECRET = 'test-internal-secret'; const CURRENT_AUTH_TOKEN = 'current-customer-token'; +const BYOC_PROVIDER_BINDING = { + kind: 'vercel', + source: { + kind: 'byoc', + organizationId: ORGANIZATION_ID, + credentialId: 'ffffffff-ffff-4fff-8fff-ffffffffffff', + }, +} as const; const router = t.router(createSessionWorktreeHandlers()); @@ -179,6 +187,7 @@ function sourceMetadataWithPreset(sandboxAllocation: SandboxAllocation): Session ...metadata.workspace, sandboxId, sandboxProvider, + ...(sandboxProvider === 'vercel' ? { sandboxProviderBinding: BYOC_PROVIDER_BINDING } : {}), sandboxAllocation, sandboxRoute: sandboxAllocation === 'cloudflare-shared' @@ -684,7 +693,9 @@ describe('createWorktreeChat sandbox preset inheritance', () => { context.env.SANDBOX_SELECTION_IDS = ''; context.env.PER_SESSION_SANDBOX_ORG_IDS = sandboxAllocation === 'cloudflare-shared' ? '*' : ''; - context.env.VERCEL_SANDBOX_ORG_IDS = sandboxAllocation.startsWith('vercel-') ? '' : '*'; + Object.assign(context.env, { + VERCEL_SANDBOX_ORG_IDS: sandboxAllocation.startsWith('vercel-') ? '' : '*', + }); context.env.CREDENTIAL_CONTAINMENT_ENABLED = 'true'; await caller.createWorktreeChat(input); const registration = destinationStub.registerSessionFromMetadata.mock.calls[0]?.[0]; @@ -848,7 +859,7 @@ describe('createWorktreeChat sandbox preset inheritance', () => { await caller.createWorktreeChat(input); const progress = recordOperationProgressMock.mock.calls[0]?.[2] as Record; context.env.SANDBOX_SELECTION_IDS = ''; - context.env.VERCEL_TOKEN = ''; + Object.assign(context.env, { VERCEL_TOKEN: '' }); admitOperationMock.mockResolvedValueOnce({ admission: 'duplicate_settled', row: ledgerRow({ @@ -1067,45 +1078,74 @@ describe('createWorktreeChat ownership, metadata, and control-plane routing', () expect(destinationStub.registerSessionFromMetadata).not.toHaveBeenCalled(); }); - it('preserves an organization-scoped isolated Vercel route without copying provider runtime', async () => { - const metadata = sourceMetadata({ organizationId: ORGANIZATION_ID }); - metadata.workspace = { - ...metadata.workspace, - sandboxId: 'ses-0123456789abcdef', - sandboxProvider: 'vercel', - sandboxRoute: undefined, - providerRuntime: { provider: 'vercel', sessionId: 'vercel-instance' }, - }; - const { caller, input, destinationStub } = fixture({ - organizationId: ORGANIZATION_ID, - metadata, - }); + it.each([undefined, { kind: 'vercel', source: { kind: 'platform' } }] as const)( + 'rejects a retired Vercel sibling binding %j without creating customer compute', + async sandboxProviderBinding => { + const metadata = sourceMetadata({ organizationId: ORGANIZATION_ID }); + metadata.workspace = { + ...metadata.workspace, + sandboxId: 'ses-0123456789abcdef', + sandboxProvider: 'vercel', + sandboxProviderBinding, + sandboxRoute: undefined, + }; + const { caller, input, destinationStub } = fixture({ + organizationId: ORGANIZATION_ID, + metadata, + }); + await expect(caller.createWorktreeChat(input)).rejects.toThrow( + 'Kilo-paid Vercel compute is retired' + ); + expect(destinationStub.registerSessionFromMetadata).not.toHaveBeenCalled(); + expect(createSessionForCloudAgentMock).not.toHaveBeenCalled(); + expect(admitOperationMock).not.toHaveBeenCalled(); + } + ); - await caller.createWorktreeChat(input); + it.each([BYOC_PROVIDER_BINDING] as const)( + 'inherits the isolated Vercel provider binding %j without copying provider runtime', + async sandboxProviderBinding => { + const metadata = sourceMetadata({ organizationId: ORGANIZATION_ID }); + metadata.workspace = { + ...metadata.workspace, + sandboxId: 'ses-0123456789abcdef', + sandboxProvider: 'vercel', + sandboxProviderBinding, + sandboxRoute: undefined, + providerRuntime: { provider: 'vercel', sessionId: 'vercel-instance' }, + }; + const { caller, input, destinationStub } = fixture({ + organizationId: ORGANIZATION_ID, + metadata, + }); - expect(assertOrganizationMembershipMock).toHaveBeenCalledWith( - expect.anything(), - USER_ID, - ORGANIZATION_ID - ); - expect(destinationStub.registerSessionFromMetadata).toHaveBeenCalledWith( - expect.objectContaining({ - metadata: expect.objectContaining({ - identity: expect.objectContaining({ orgId: ORGANIZATION_ID }), - workspace: expect.objectContaining({ - sandboxId: 'ses-0123456789abcdef', - sandboxProvider: 'vercel', - worktreeId: WORKTREE_ID, - workspacePath: getWorktreeWorkspacePath(ORGANIZATION_ID, USER_ID, WORKTREE_ID), + await caller.createWorktreeChat(input); + + expect(assertOrganizationMembershipMock).toHaveBeenCalledWith( + expect.anything(), + USER_ID, + ORGANIZATION_ID + ); + expect(destinationStub.registerSessionFromMetadata).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + identity: expect.objectContaining({ orgId: ORGANIZATION_ID }), + workspace: expect.objectContaining({ + sandboxId: 'ses-0123456789abcdef', + sandboxProvider: 'vercel', + sandboxProviderBinding, + worktreeId: WORKTREE_ID, + workspacePath: getWorktreeWorkspacePath(ORGANIZATION_ID, USER_ID, WORKTREE_ID), + }), }), - }), - }) - ); - const registration = destinationStub.registerSessionFromMetadata.mock.calls[0]?.[0]; - expect(registration?.metadata.workspace).not.toHaveProperty('providerRuntime'); - expect(registration?.metadata.repository).not.toHaveProperty('token'); - expect(registration?.metadata.auth.kilocodeToken).toBe(CURRENT_AUTH_TOKEN); - }); + }) + ); + const registration = destinationStub.registerSessionFromMetadata.mock.calls[0]?.[0]; + expect(registration?.metadata.workspace).not.toHaveProperty('providerRuntime'); + expect(registration?.metadata.repository).not.toHaveProperty('token'); + expect(registration?.metadata.auth.kilocodeToken).toBe(CURRENT_AUTH_TOKEN); + } + ); it.each(['kilo/generated-assigned-branch', 'feature/assigned-branch'])( 'inherits the assigned workspace branch %s instead of the upstream branch', @@ -1551,6 +1591,62 @@ describe('createWorktreeChat registration rollback and unknown-outcome reconcili expect(createSessionForCloudAgentMock).not.toHaveBeenCalled(); }); + describe.each(['transport retry', 'reconciliation'] as const)('%s BYOC inheritance', path => { + it.each([ + undefined, + { kind: 'vercel', source: { kind: 'platform' } }, + { + ...BYOC_PROVIDER_BINDING, + source: { ...BYOC_PROVIDER_BINDING.source, organizationId: OTHER_ORGANIZATION_ID }, + }, + { + ...BYOC_PROVIDER_BINDING, + source: { ...BYOC_PROVIDER_BINDING.source, credentialId: OPERATION_KEY }, + }, + ] as const)('rejects a lost or changed provider binding %j', async sandboxProviderBinding => { + const metadata = sourceMetadata({ organizationId: ORGANIZATION_ID }); + metadata.workspace = { + ...metadata.workspace, + sandboxId: 'ses-0123456789abcdef', + sandboxProvider: 'vercel', + sandboxProviderBinding: BYOC_PROVIDER_BINDING, + sandboxRoute: undefined, + }; + const { caller, input, destinationStub } = fixture({ + organizationId: ORGANIZATION_ID, + metadata, + }); + const destination = destinationMetadata(metadata); + destination.workspace = { ...destination.workspace, sandboxProviderBinding }; + destinationStub.getMetadata.mockResolvedValueOnce(destination); + if (path === 'transport retry') { + destinationStub.registerSessionFromMetadata.mockRejectedValueOnce( + Object.assign(new Error('registration outcome unknown'), { retryable: true }) + ); + } else { + admitOperationMock.mockResolvedValueOnce({ + admission: 'duplicate_reconcile_pending', + row: ledgerRow({ + organization_id: ORGANIZATION_ID, + status: 'reconcile_pending', + canonical_result: await progressFor(input), + }), + }); + } + + await expect(caller.createWorktreeChat(input)).rejects.toMatchObject({ code: 'CONFLICT' }); + + expect(destinationStub.registerSessionFromMetadata).toHaveBeenCalledTimes( + path === 'transport retry' ? 1 : 0 + ); + expect(createSessionForCloudAgentMock).toHaveBeenCalledTimes( + path === 'transport retry' ? 1 : 0 + ); + expect(deleteSessionForCloudAgentMock).not.toHaveBeenCalled(); + expect(settleOperationMock).not.toHaveBeenCalled(); + }); + }); + it('does not report success when the completed ledger settlement is not recorded', async () => { settleOperationMock.mockResolvedValueOnce({ settled: false, row: null }); const { caller, input, destinationStub } = fixture(); diff --git a/services/cloud-agent-next/src/router/handlers/session-worktree.ts b/services/cloud-agent-next/src/router/handlers/session-worktree.ts index 636a2d2ebad..96d056d5ac3 100644 --- a/services/cloud-agent-next/src/router/handlers/session-worktree.ts +++ b/services/cloud-agent-next/src/router/handlers/session-worktree.ts @@ -1,3 +1,4 @@ +import { VERCEL_BYOC_REQUIRED_MESSAGE } from '../../byoc/vercel-owner.js'; import { TRPCError } from '@trpc/server'; import type { WorkerDb } from '@kilocode/db/client'; import { @@ -26,9 +27,11 @@ import { z } from 'zod'; import { getPgDb } from '../../db/pg.js'; import { CurrentSessionMetadataSchema, + getSandboxProviderBinding, type SessionMetadata, } from '../../persistence/session-metadata.js'; import { logControlDiagnostic } from '../../sandbox-control/diagnostics.js'; +import { sameSandboxProviderBinding } from '../../sandbox-provider-binding.js'; import { getSandboxSessionStub } from '../../sandbox-session/session-stub.js'; import { generateSessionId } from '../../session-identity.js'; import { buildControlPlaneSandboxSelection } from '../../session/control-plane-session-input.js'; @@ -252,6 +255,11 @@ async function loadWorktreeSource( throw sourceRejected(); } + const binding = getSandboxProviderBinding(metadata); + if (binding.kind === 'vercel' && binding.source.kind === 'platform') { + throw new TRPCError({ code: 'BAD_REQUEST', message: VERCEL_BYOC_REQUIRED_MESSAGE }); + } + return { ownership, metadata, @@ -474,6 +482,10 @@ function assertRegisteredMetadata( (workspace.egressMode ?? 'unrestricted') !== (source.workspace.egressMode ?? 'unrestricted') || JSON.stringify(workspace.egressAllowlist ?? []) !== JSON.stringify(source.workspace.egressAllowlist ?? []) || + !sameSandboxProviderBinding( + getSandboxProviderBinding(metadata), + getSandboxProviderBinding(source.metadata) + ) || workspace.branchName !== sourceWorktreeBranchName(source) || JSON.stringify(workspace.sandboxRoute) !== JSON.stringify(source.workspace.sandboxRoute) || workspace.sandboxKind !== source.workspace.sandboxKind || diff --git a/services/cloud-agent-next/src/router/schemas.test.ts b/services/cloud-agent-next/src/router/schemas.test.ts index ce686f13a0e..6e9a7bdc95a 100644 --- a/services/cloud-agent-next/src/router/schemas.test.ts +++ b/services/cloud-agent-next/src/router/schemas.test.ts @@ -399,15 +399,15 @@ describe('sandbox preset input boundaries', () => { ); it.each(['small', 'large'] as const)( - 'rejects unsupported BYOC %s rather than allocating Kilo compute', + 'rejects retired Kilo-paid Vercel %s requests', instanceType => { - const sandboxAllocation = { provider: { id: 'vercel', account: 'byoc' }, instanceType }; + const sandboxAllocation = { provider: { id: 'vercel', account: 'kilo' }, instanceType }; for (const result of [ PrepareSessionInput.safeParse({ ...prepare, sandboxAllocation }), StartSessionInput.safeParse({ ...baseStartInput, runtime: { sandboxAllocation } }), ]) { expect(result.success).toBe(false); - if (!result.success) expect(result.error.message).toContain('BYOC'); + if (!result.success) expect(result.error.message).toContain('byoc'); } } ); diff --git a/services/cloud-agent-next/src/router/schemas.ts b/services/cloud-agent-next/src/router/schemas.ts index ef8b634ec6c..c252227a1ac 100644 --- a/services/cloud-agent-next/src/router/schemas.ts +++ b/services/cloud-agent-next/src/router/schemas.ts @@ -1,7 +1,7 @@ import * as z from 'zod'; import { egressPolicySchema } from '@kilocode/worker-utils/egress-allowlist'; import { - getKiloSandboxAllocation, + getSandboxAllocation, sandboxAllocationInputSchema, } from '@kilocode/worker-utils/sandbox-allocation'; import { @@ -62,9 +62,9 @@ export { export const MessageIdSchema = z.string().regex(MESSAGE_ID_PATTERN, MESSAGE_ID_FORMAT_DESCRIPTION); const ManagedSandboxAllocationInput = sandboxAllocationInputSchema.transform((request, ctx) => { - const allocation = getKiloSandboxAllocation(request); + const allocation = getSandboxAllocation(request); if (allocation === undefined) { - ctx.addIssue({ code: 'custom', message: 'BYOC sandbox allocation is not available' }); + ctx.addIssue({ code: 'custom', message: 'Sandbox allocation is not available' }); return z.NEVER; } return allocation; diff --git a/services/cloud-agent-next/src/runtime-credential-proxy.test.ts b/services/cloud-agent-next/src/runtime-credential-proxy.test.ts index 49286388923..4b846da8a99 100644 --- a/services/cloud-agent-next/src/runtime-credential-proxy.test.ts +++ b/services/cloud-agent-next/src/runtime-credential-proxy.test.ts @@ -575,6 +575,18 @@ describe('runtime credential proxy', () => { kiloSessionId: 'kilo_1', })?.toString() ).toBe('https://provider.example.test/api/openrouter/chat/completions'); + for (const base of ['https://backend.example.test/api', 'http://127.0.0.1:4300/api/']) { + expect( + resolveRuntimeCredentialProxyRoute({ + targets: { ...targets, providerBaseUrl: base }, + route: 'provider', + method: 'POST', + pathname: '/api/openrouter/chat/completions', + search: '?stream=true', + kiloSessionId: 'kilo_1', + })?.toString() + ).toBe(`${base.replace(/\/$/, '')}/openrouter/chat/completions?stream=true`); + } expect( resolveRuntimeCredentialProxyRoute({ targets: { ...targets, providerBaseUrl: 'https://api.kilo.ai' }, diff --git a/services/cloud-agent-next/src/sandbox-control/downloaded-startup.test.ts b/services/cloud-agent-next/src/sandbox-control/downloaded-startup.test.ts index d4f11dff491..40f43853ac9 100644 --- a/services/cloud-agent-next/src/sandbox-control/downloaded-startup.test.ts +++ b/services/cloud-agent-next/src/sandbox-control/downloaded-startup.test.ts @@ -61,8 +61,8 @@ describe('startup selection and preflight', () => { ] as const)('rejects unsupported configuration before provider work: %s', (env, provider) => { expect(() => selectStartup(env, provider)).toThrow('configuration'); }); - it('keeps preinstalled startup for SDK and Vercel pins on a downloaded target', () => { - for (const provider of ['cloudflare', 'vercel'] as const) + it('keeps preinstalled startup for SDK pins on a downloaded target', () => { + for (const provider of ['cloudflare'] as const) expect( selectStartup( { RUNTIME_STARTUP_MODE: 'downloaded', RUNTIME_RELEASE_DESCRIPTOR: text }, @@ -70,6 +70,17 @@ describe('startup selection and preflight', () => { ).mode ).toBe('preinstalled'); }); + it('selects downloaded startup for Vercel and requires its release descriptor', () => { + expect( + selectStartup( + { RUNTIME_STARTUP_MODE: 'downloaded', RUNTIME_RELEASE_DESCRIPTOR: text }, + 'vercel' + ) + ).toMatchObject({ mode: 'downloaded', descriptor: text }); + expect(() => selectStartup({ RUNTIME_STARTUP_MODE: 'downloaded' }, 'vercel')).toThrow( + 'configuration' + ); + }); it('keeps image and mode independent and isolates downloaded snapshot keys', () => { for (const image of ['all-in-one', 'runtime-free']) expect( @@ -136,6 +147,7 @@ describe('startup selection and preflight', () => { deadlineAt, }); expect(command.argv[0]).toBe('/bin/sh'); + expect(command.user).toBe('root'); expect(command.argv.every(arg => new TextEncoder().encode(arg).byteLength < 128 * 1024)).toBe( true ); diff --git a/services/cloud-agent-next/src/sandbox-control/downloaded-startup.ts b/services/cloud-agent-next/src/sandbox-control/downloaded-startup.ts index f3ea1ecb5cf..0fbd3ed5d95 100644 --- a/services/cloud-agent-next/src/sandbox-control/downloaded-startup.ts +++ b/services/cloud-agent-next/src/sandbox-control/downloaded-startup.ts @@ -46,5 +46,5 @@ export async function downloadedStartup(input: { } if (command.argv.some(arg => new TextEncoder().encode(arg).byteLength >= 128 * 1024)) throw new ProviderCreationError('invalid_configuration'); - return command; + return { ...command, user: 'root' }; } diff --git a/services/cloud-agent-next/src/sandbox-control/provider-contract.test.ts b/services/cloud-agent-next/src/sandbox-control/provider-contract.test.ts index 4ac86bd5334..f9b7e57cbf0 100644 --- a/services/cloud-agent-next/src/sandbox-control/provider-contract.test.ts +++ b/services/cloud-agent-next/src/sandbox-control/provider-contract.test.ts @@ -124,6 +124,7 @@ type ContractHarness = { billingBlock?(blocked: boolean): void; /** Container providers: how many backend billing reads the adapter made. */ billingReads?(): number; + launchSudo?(): boolean | undefined; }; async function requireRef(adapter: ProviderAdapter): Promise { @@ -412,6 +413,7 @@ function vercelHarness(): ContractHarness { leaseMs: () => [...state.extendedMs], grantReads: () => state.grantReads, appliedPolicies: () => [...state.policies], + launchSudo: () => state.commands.at(-1)?.sudo, }; } @@ -646,6 +648,16 @@ describe('provider-specific contract differences', () => { expect(harness.billingReads?.()).toBe(reads); }); + it('runs the trusted downloaded installer as root on Vercel', async () => { + const harness = vercelHarness(); + const ref = await harness.create(); + await harness.adapter.launch(ref, { + command: { argv: ['/bin/sh', '-c', 'installer'], user: 'root' }, + env: {}, + }); + expect(harness.launchSudo?.()).toBe(true); + }); + it('reads Vercel grants on every create and applies a contained-credential policy', async () => { const harness = vercelHarness(); const ref = await harness.create(); @@ -654,6 +666,7 @@ describe('provider-specific contract differences', () => { await harness.adapter.launch(ref, { command: preinstalledStartup(), env: {} }); expect(harness.launchSupervisor()).toBe(CONTROL_SUPERVISOR_PATH); + expect(harness.launchSudo?.()).toBe(false); expect('applyContainedCredentials' in harness.adapter).toBe(true); // A foreign ref is refused before any Vercel request. const leases = harness.leaseMs().length; diff --git a/services/cloud-agent-next/src/sandbox-control/provider-creation.test.ts b/services/cloud-agent-next/src/sandbox-control/provider-creation.test.ts index d062ea22029..ef5b4d26b7c 100644 --- a/services/cloud-agent-next/src/sandbox-control/provider-creation.test.ts +++ b/services/cloud-agent-next/src/sandbox-control/provider-creation.test.ts @@ -380,12 +380,10 @@ describe('Vercel runtime metadata', () => { readFile: vi.fn(), updateNetworkPolicy: vi.fn(), }; - const billingLifetimeSink = vi.fn(); const provider = createVercelProviderAdapter({ sandboxName: SANDBOX_ID, config, restClient, - billingLifetimeSink, readContainedGrants: async () => [], }); const result = @@ -425,7 +423,7 @@ describe('Vercel runtime metadata', () => { expect(restClient.getSession).not.toHaveBeenCalled(); if (operation === 'create') { expect(restClient.createSandbox).toHaveBeenCalledWith( - expect.objectContaining({ snapshotId: config.snapshotId }) + expect.objectContaining({ source: { type: 'snapshot', snapshotId: config.snapshotId } }) ); } else { expect(restClient.inspectByName).toHaveBeenCalledWith( @@ -439,11 +437,6 @@ describe('Vercel runtime metadata', () => { expect(await provider.observe(providerRef, intent)).toEqual({ status: 'active' }); expect(await provider.stop(providerRef, intent)).toBe('terminal'); expect(restClient.stopSession).toHaveBeenCalledOnce(); - expect(billingLifetimeSink.mock.calls.map(([evidence]) => evidence.providerRef)).toEqual([ - providerRef, - providerRef, - providerRef, - ]); expect(provider.describeRuntime?.(providerRef)?.region).toBe( validMetadata ? region : undefined ); diff --git a/services/cloud-agent-next/src/sandbox-control/provider.ts b/services/cloud-agent-next/src/sandbox-control/provider.ts index 7340418d87f..4bfe69c58c2 100644 --- a/services/cloud-agent-next/src/sandbox-control/provider.ts +++ b/services/cloud-agent-next/src/sandbox-control/provider.ts @@ -107,7 +107,11 @@ export type ProviderLaunchResult = { }; /** The first process that a provider runs in a new or restored sandbox. */ -export type ProviderStartupCommand = { argv: readonly string[] }; +export type ProviderStartupCommand = { + argv: readonly string[]; + /** Trusted bootstrap needs to install runtime files outside the workspace. */ + user?: 'root'; +}; export type ProviderAdapter = { describeRuntime?(ref: string): CloudAgentRunContainer | undefined; diff --git a/services/cloud-agent-next/src/sandbox-control/session-credentials.test.ts b/services/cloud-agent-next/src/sandbox-control/session-credentials.test.ts new file mode 100644 index 00000000000..164e4c4887a --- /dev/null +++ b/services/cloud-agent-next/src/sandbox-control/session-credentials.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest'; +import { + buildControlNetworkPolicy, + prepareCredentialGrant, + sessionCredentialsPayloadFromGrant, + type CredentialGrantPreparation, +} from './session-credentials.js'; + +const sessionId = 'workspace_11111111-1111-4111-8111-111111111111'; +const kiloSessionId = 'ses_aaaaaaaaaaaaaaaaaaaaaaaaaa'; +const facade = 'https://worker.example.com'; + +function localPreparation(): CredentialGrantPreparation { + return { + env: { + WORKER_URL: facade, + KILOCODE_BACKEND_BASE_URL: 'http://localhost:4300', + KILO_OPENROUTER_BASE: 'http://localhost:4300/api/openrouter', + KILO_SESSION_INGEST_URL: 'http://localhost:10100', + } as CredentialGrantPreparation['env'], + source: { + userId: 'user-1', + kiloSessionId, + kiloToken: 'eyJhbGciOiJub25lIn0.eyJydW50aW1lQXV0aG9yaXphdGlvbiI6eyJpZCI6InJhXzEifX0.c2ln', + repository: { type: 'git', url: 'https://github.com/acme/widgets.git' }, + scopeId: sessionId, + }, + sessionId, + sandboxId: 'ses-abcdef', + provider: 'vercel', + containmentEnabled: true, + directory: '/workspace/test', + scopeId: sessionId, + payload: {}, + now: 1_000, + }; +} + +describe('Vercel credentials with private control-plane upstreams', () => { + it('keeps loopback upstreams behind the HTTPS runtime facade', async () => { + const input = localPreparation(); + const { grant } = await prepareCredentialGrant(input); + const proxy = grant.kilo.runtimeProxy; + if (!proxy) throw new Error('Missing runtime proxy'); + proxy.members.push({ sessionId, kiloSessionId, handle: 'test-session-handle' }); + + const payload = sessionCredentialsPayloadFromGrant(grant, sessionId); + expect(payload.proxy?.targets).toEqual({ + backendBaseUrl: facade, + providerBaseUrl: facade, + sessionIngestBaseUrl: facade, + }); + expect(JSON.stringify(payload)).not.toContain('localhost'); + expect(JSON.stringify(payload)).not.toContain(input.source.kiloToken); + const policy = buildControlNetworkPolicy([grant]); + expect(policy.injectionRules.length).toBeGreaterThan(0); + expect(policy.injectionRules.every(rule => rule.domain === 'worker.example.com')).toBe(true); + expect(JSON.stringify(policy)).not.toContain('localhost'); + }); + + it('rejects a public HTTP upstream', async () => { + const input = localPreparation(); + input.env.KILOCODE_BACKEND_BASE_URL = 'http://backend.example.com'; + await expect(prepareCredentialGrant(input)).rejects.toThrow(); + }); + + it('rejects an HTTP sandbox-facing facade', async () => { + const input = localPreparation(); + input.env.WORKER_URL = 'http://worker.example.com'; + await expect(prepareCredentialGrant(input)).rejects.toThrow(); + }); +}); diff --git a/services/cloud-agent-next/src/sandbox-control/session-credentials.ts b/services/cloud-agent-next/src/sandbox-control/session-credentials.ts index e9c162cf0dd..7d18aa2b2d7 100644 --- a/services/cloud-agent-next/src/sandbox-control/session-credentials.ts +++ b/services/cloud-agent-next/src/sandbox-control/session-credentials.ts @@ -249,8 +249,7 @@ function refinePersistedSessionCredentialGrant( KILO_OPENROUTER_BASE: grant.kilo.targets.providerBaseUrl, KILO_SESSION_INGEST_URL: grant.kilo.targets.sessionIngestBaseUrl, }, - grant.kilo.token, - { requireHttps: grant.provider === 'vercel' } + grant.kilo.token ); if (!targets.success || JSON.stringify(targets.targets) !== JSON.stringify(grant.kilo.targets)) { reject(); @@ -1163,9 +1162,9 @@ export async function prepareCredentialGrant( } const provider = input.provider; const containmentEnabled = input.containmentEnabled; - const targets = deriveKiloSandboxTargets(env, token.data, { - requireHttps: provider === 'vercel', - }); + // Upstream requests run in the control plane. The runtime uses the separately + // validated runtime-proxy facade, including for Vercel. + const targets = deriveKiloSandboxTargets(env, token.data); if (!targets.success) { diagnostic?.record({ grantPhase: 'configuration', grantFailureReason: 'invalid_target' }); invalidCredentials(); diff --git a/services/cloud-agent-next/src/sandbox-control/startup-selection.ts b/services/cloud-agent-next/src/sandbox-control/startup-selection.ts index 0688089d48a..8d8d3d86539 100644 --- a/services/cloud-agent-next/src/sandbox-control/startup-selection.ts +++ b/services/cloud-agent-next/src/sandbox-control/startup-selection.ts @@ -19,9 +19,8 @@ export function selectStartup( }); if (!selected.success) throw new ProviderCreationError('invalid_configuration'); const { mode, image } = selected.data; - // Downloaded startup is native-only. SDK and Vercel pins keep preinstalled startup. - if (mode === 'preinstalled' || provider !== 'cloudflare-containers') - return { mode: 'preinstalled', image }; + // The retired SDK provider keeps its preinstalled launch contract. + if (mode === 'preinstalled' || provider === 'cloudflare') return { mode: 'preinstalled', image }; if (!env.RUNTIME_RELEASE_DESCRIPTOR) throw new ProviderCreationError('invalid_configuration'); return { mode, image, descriptor: env.RUNTIME_RELEASE_DESCRIPTOR }; } diff --git a/services/cloud-agent-next/src/sandbox-control/vercel-billing.test.ts b/services/cloud-agent-next/src/sandbox-control/vercel-billing.test.ts deleted file mode 100644 index e10f3fe6b23..00000000000 --- a/services/cloud-agent-next/src/sandbox-control/vercel-billing.test.ts +++ /dev/null @@ -1,965 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { - BILLING_HEARTBEAT_CALLBACK, - clearBillingContext, - createContainerUsageClient, - getBillingContext, - installBillingHeartbeat, - setBillingContext, - updateBillingContext, - type BillingContextStorage, - type ContainerUsageRpcMethods, - type HeartbeatAck, - type RecordAck, - type RecordStartResult, -} from '@kilocode/container-usage'; -import type { VercelSandboxResources } from '@kilocode/worker-utils/sandbox-allocation'; -import { VERCEL_BILLING_CAPACITIES } from '../container-usage-context.js'; -import { MeteredBillingLifecycle, type BillingIdentity } from '../metered-billing-lifecycle.js'; -import type { AllocationState, SandboxTimers } from '../control-plane/sandbox/allocation.js'; -import { createVercelBillingRuntime } from '../control-plane/sandbox/vercel-billing-runtime.js'; -import type { ProviderAdapter } from './provider.js'; -import type { VercelSandboxSession } from '../agent-sandbox/vercel/vercel-sandbox-rest-client.js'; -import type { BillingScheduleTable } from './billing-schedule.js'; -import { createVercelProviderAdapter, type VercelControlRestClient } from './vercel-provider.js'; -import { - loadVercelBillingBinding, - saveVercelBillingBinding, - VERCEL_BILLING_SETTLEMENT_CALLBACK, - VercelBilling, - type VercelBillingBinding, -} from './vercel-billing.js'; - -const T0 = 1_000_000; -const SERVICE = 'cloud-agent-next-sandbox-vercel-small'; - -describe('Vercel provider lifetime sink', () => { - it('records the real adapter create and terminal stop timestamps', async () => { - const session = { - id: 'vercel-session', - status: 'running', - createdAt: 1_000, - } as VercelSandboxSession; - const lifetime = vi.fn( - async (_evidence: { providerRef: string; createdAtMs?: number; terminalAtMs?: number }) => - undefined - ); - const provider = createVercelProviderAdapter({ - sandboxName: 'ses-abc', - config: { - accessToken: 'test-token', - teamId: 'team', - projectId: 'project', - snapshotId: 'snapshot', - runtimeBuildId: 'build', - runtime: 'node24', - initialTimeoutMs: 60_000, - extendDurationMs: 60_000, - }, - restClient: { - createSandbox: async () => ({ - runtime: { sandboxName: 'ses-abc', sessionId: session.id }, - session, - }), - getSession: async () => ({ session: { ...session, status: 'stopped', stoppedAt: 3_000 } }), - stopSession: async () => ({ ...session, status: 'stopped', stoppedAt: 4_000 }), - } as unknown as VercelControlRestClient, - billingLifetimeSink: lifetime, - readContainedGrants: async () => [], - }); - const created = await provider.create({ - intentId: 'attempt', - createdAt: 0, - allocationName: 'ses-abc', - }); - if ('unresolved' in created) throw new Error('Expected a Vercel provider reference'); - expect(lifetime).toHaveBeenCalledWith({ providerRef: created.providerRef, createdAtMs: 1_000 }); - expect((await provider.observe(created.providerRef)).status).toBe('terminal'); - expect(lifetime).toHaveBeenLastCalledWith({ - providerRef: created.providerRef, - createdAtMs: 1_000, - terminalAtMs: 3_000, - }); - expect(await provider.stop(created.providerRef)).toBe('terminal'); - expect(lifetime).toHaveBeenLastCalledWith({ - providerRef: created.providerRef, - createdAtMs: 1_000, - terminalAtMs: 4_000, - }); - }); -}); -const IDENTITY: BillingIdentity = { sandboxClassName: 'SandboxVercelSmall' }; -const BILLING_INPUT = { - sandboxId: 'ses-abcdef', - subject: { type: 'user' as const, id: 'user-1' }, - actor: { type: 'user' as const, id: 'user-1' }, - sessionId: 'agent_1', - metadata: { origin: 'cloud-agent' }, -}; - -type StoredContext = Awaited>; - -function memoryStorage(): BillingContextStorage & { values: Map } { - const values = new Map(); - return { - values, - get: async (key: string) => values.get(key) as T | undefined, - put: async (key, value) => { - values.set(key, value); - }, - delete: async key => values.delete(key), - }; -} - -type MeterRecordStartInput = Parameters[0]; -type MeterRecordStopInput = Parameters[0]; - -class FakeMeter implements ContainerUsageRpcMethods { - recordStartInputs: MeterRecordStartInput[] = []; - recordHeartbeatInputs: unknown[] = []; - recordStopInputs: MeterRecordStopInput[] = []; - startResult: RecordStartResult = { - success: true, - ack: { intervalId: 'interval-1', durable: 'pg', dedup: false }, - }; - recordStartBehavior: 'ok' | 'reject' = 'ok'; - recordStopBehavior: 'ok' | 'reject' = 'ok'; - - async recordStart(input: MeterRecordStartInput): Promise { - this.recordStartInputs.push(input); - if (this.recordStartBehavior === 'reject') throw new Error('meter unavailable'); - return this.startResult; - } - - async recordHeartbeat(input: unknown): Promise { - this.recordHeartbeatInputs.push(input); - return { - intervalId: 'interval-1', - durable: 'pg', - dedup: false, - budget: { verdict: 'continue' }, - }; - } - - async recordStop(input: MeterRecordStopInput): Promise { - this.recordStopInputs.push(input); - if (this.recordStopBehavior === 'reject') throw new Error('meter stop unavailable'); - return { intervalId: 'interval-1', durable: 'pg', dedup: false }; - } -} - -class FakeContainer { - running = true; - schedules: Array<{ callback: string; payload?: unknown }> = []; - deleteSchedules = (_callback: string): void => undefined; - schedule = async ( - _delaySeconds: number, - callback: string, - payload?: unknown - ): Promise => { - this.schedules.push({ callback, payload }); - return { - taskId: callback, - callback, - payload, - type: 'delayed' as const, - time: Date.now(), - delayInSeconds: _delaySeconds, - }; - }; - getState = async (): Promise<{ status: 'running'; lastChange: number }> => ({ - status: 'running', - lastChange: Date.now(), - }); -} - -type ScheduleCall = { - op: 'markDue' | 'deferRetry' | 'remove' | 'schedule'; - callback: string; - payload?: unknown; - dueAtMs?: number; - notBeforeMs?: number; -}; - -function fakeScheduleTable() { - const calls: ScheduleCall[] = []; - const entries = new Map< - string, - { dueAtMs: number; retryNotBeforeMs?: number; payload?: unknown } - >(); - return { - calls, - entries, - async markDue(callback: string, payload: unknown, dueAtMs: number): Promise { - calls.push({ op: 'markDue', callback, payload, dueAtMs }); - const current = entries.get(callback); - if (current === undefined || current.payload !== payload) return; - entries.set(callback, { dueAtMs, payload }); - }, - async deferRetry(callback: string, payload: unknown, notBeforeMs: number): Promise { - calls.push({ op: 'deferRetry', callback, payload, notBeforeMs }); - }, - async remove(callback: string, payload?: unknown): Promise { - calls.push({ op: 'remove', callback, payload }); - const current = entries.get(callback); - if (current === undefined) return; - if (payload !== undefined && current.payload !== payload) return; - entries.delete(callback); - }, - async schedule(callback: string, dueAtMs: number, payload?: unknown): Promise { - calls.push({ op: 'schedule', callback, payload, dueAtMs }); - entries.set(callback, payload === undefined ? { dueAtMs } : { dueAtMs, payload }); - }, - }; -} - -function setup() { - const storage = memoryStorage(); - const meter = new FakeMeter(); - const container = new FakeContainer(); - const pending: Promise[] = []; - const usageClient = createContainerUsageClient(meter, { - service: SERVICE, - retry: { attempts: 1 }, - }); - const lifecycle = new MeteredBillingLifecycle({ - storage, - usageClient, - schedule: (_delaySeconds, callback, payload) => container.schedule(0, callback, payload), - deleteSchedules: callback => container.deleteSchedules(callback), - getState: () => container.getState(), - isContainerRunning: () => container.running, - stopContainer: async () => undefined, - destroyContainer: async () => undefined, - durableObjectId: 'do-id', - waitUntil: promise => { - pending.push(promise); - }, - }); - const billingRef: { current?: VercelBilling } = {}; - const heartbeat = installBillingHeartbeat( - container as unknown as Parameters[0], - { - client: usageClient, - storage, - heartbeatSeconds: 300, - enforceBudgetStop: async () => undefined, - onGenerationClosed: context => billingRef.current?.onGenerationClosed(context), - } - ); - lifecycle.attachHeartbeat(heartbeat); - const schedule = fakeScheduleTable(); - const billing = new VercelBilling({ - storage, - lifecycle, - heartbeat, - schedule: schedule as unknown as BillingScheduleTable, - }); - billingRef.current = billing; - return { storage, meter, container, pending, lifecycle, heartbeat, billing, schedule }; -} - -async function seedContext( - storage: BillingContextStorage, - overrides: Partial> = {} -): Promise> { - const context = await setBillingContext(storage, { - service: SERVICE, - instanceId: 'ses-abcdef', - sku: 'cloud-agent-vercel-small-2026-09', - subject: { type: 'user', id: 'user-1' }, - actor: { type: 'user', id: 'user-1' }, - sessionId: 'agent_1', - metadata: { origin: 'cloud-agent' }, - startEpochMs: T0, - }); - if (Object.keys(overrides).length === 0) return context; - await updateBillingContext(storage, { ...context, ...overrides }); - return (await getBillingContext(storage)) as NonNullable; -} - -function bindingFor(generation: string, overrides: Partial = {}) { - return { - generation, - providerRef: 'provider-ref-1', - createdAtMs: T0, - ...overrides, - } satisfies VercelBillingBinding; -} - -beforeEach(() => { - vi.useFakeTimers(); - vi.setSystemTime(T0); -}); - -afterEach(() => { - vi.useRealTimers(); -}); - -describe('MeteredBillingLifecycle placed sandbox admission', () => { - const PLACED_ID = `sbx-${'e'.repeat(48)}`; - function placedInput(sandboxKind: string | undefined, sandboxId = PLACED_ID) { - return { - sandboxId, - subject: { type: 'user', id: 'user-1' }, - actor: { type: 'user', id: 'user-1' }, - sessionId: 'workspace_placed', - metadata: { origin: 'cloud-agent' }, - enforcementRequested: true, - ...(sandboxKind ? { sandboxKind } : {}), - }; - } - - it.each(['SandboxSmallContainment', 'SandboxVercelSmall'] as const)( - 'admits a placed sandbox in %s and meters it under its neutral key', - async sandboxClassName => { - const { lifecycle, meter, container } = setup(); - container.running = false; - - await expect( - lifecycle.ensureBillingAdmission({ sandboxClassName }, placedInput('isolated')) - ).resolves.toEqual({ success: true }); - - expect(meter.recordStartInputs).toHaveLength(1); - expect(meter.recordStartInputs[0]).toMatchObject({ - instanceId: PLACED_ID, - sessionId: 'workspace_placed', - }); - expect(meter.recordStartInputs[0]).not.toHaveProperty('sandboxKind'); - } - ); - - it('retains Code Reviewer attribution before and after billing admission', async () => { - const { lifecycle, container } = setup(); - container.running = false; - const identity = { sandboxClassName: 'Sandbox' } as const; - const input = { ...placedInput('code-review'), metadata: { origin: 'code-review' } }; - await lifecycle.configureBilling(identity, input); - const expected = { - subject: input.subject, - actor: input.actor, - instanceId: PLACED_ID, - billingOrigin: 'code-review', - }; - expect(await lifecycle.getStartAttribution()).toEqual(expected); - await expect(lifecycle.ensureBillingAdmission(identity, input)).resolves.toEqual({ - success: true, - }); - expect(await lifecycle.getStartAttribution()).toEqual(expected); - }); - - it("rejects a placed sandbox admitted outside its kind's classes", async () => { - const { lifecycle, meter } = setup(); - - await expect( - lifecycle.ensureBillingAdmission( - { sandboxClassName: 'SandboxContainment' }, - placedInput('isolated') - ) - ).rejects.toThrow('SandboxContainment billing received an incompatible sandbox ID'); - await expect( - lifecycle.ensureBillingAdmission( - { sandboxClassName: 'SandboxSmallContainment' }, - placedInput(undefined) - ) - ).rejects.toThrow('Sandbox key and kind do not match'); - expect(meter.recordStartInputs).toHaveLength(0); - }); - - it('still admits a legacy prefixed key without a kind', async () => { - const { lifecycle, meter, container } = setup(); - container.running = false; - - await expect( - lifecycle.ensureBillingAdmission( - { sandboxClassName: 'SandboxSmallContainment' }, - placedInput(undefined, `ses-${'e'.repeat(48)}`) - ) - ).resolves.toEqual({ success: true }); - expect(meter.recordStartInputs[0]).toMatchObject({ instanceId: `ses-${'e'.repeat(48)}` }); - }); -}); - -describe('pinMeasurementCursor', () => { - it('pins the supplied cursor and does not restamp it on the next schedule', async () => { - const { storage, container, lifecycle, heartbeat } = setup(); - const context = await seedContext(storage); - - vi.setSystemTime(T0 + 60_000); - await expect(lifecycle.pinMeasurementCursor(context.generation, T0)).resolves.toBe(true); - - let after = await getBillingContext(storage); - expect(after?.measurementStarted).toBe(true); - expect(after?.usageMeasuredAtMs).toBe(T0); - - await lifecycle.pinMeasurementCursor(context.generation, T0 + 1); - after = await getBillingContext(storage); - expect(after?.usageMeasuredAtMs).toBe(T0); - - container.schedules.length = 0; - await expect(heartbeat.scheduleHeartbeat()).resolves.toBeUndefined(); - after = await getBillingContext(storage); - expect(after?.usageMeasuredAtMs).toBe(T0); - expect(after?.usageMeasuredAtMs).not.toBe(Date.now()); - expect(container.schedules.at(-1)).toEqual({ - callback: 'billingHeartbeatTick', - payload: context.generation, - }); - }); - - it('refuses without taking the pin and still re-arms for each refusal reason', async () => { - const createdAt = T0 - 500_000; - const cases = [ - { name: 'already measured', overrides: { measurementStarted: true } }, - { - name: 'pending stop', - overrides: { - pendingStop: { - seq: 1, - usageSinceLast: 0, - measuredAtMs: T0, - reason: 'runtime_signal' as const, - }, - }, - }, - { - name: 'pending heartbeat', - overrides: { pendingHeartbeat: { seq: 1, usageSinceLast: 0, measuredAtMs: T0 } }, - }, - ] as const; - - for (const { name, overrides } of cases) { - const { storage, container, lifecycle } = setup(); - const context = await seedContext(storage, overrides); - vi.setSystemTime(T0 + 60_000); - container.schedules.length = 0; - - const pinned = await lifecycle.pinMeasurementCursor(context.generation, createdAt); - expect(pinned, name).toBe(false); - const after = await getBillingContext(storage); - expect(after?.usageMeasuredAtMs, name).not.toBe(createdAt); - expect(container.schedules.at(-1)?.callback, name).toBe('billingHeartbeatTick'); - } - - const { storage, container, lifecycle } = setup(); - const context = await seedContext(storage); - vi.setSystemTime(T0 + 60_000); - container.schedules.length = 0; - const pinned = await lifecycle.pinMeasurementCursor( - '00000000-0000-4000-8000-000000000000', - createdAt - ); - expect(pinned).toBe(false); - const after = await getBillingContext(storage); - expect(after?.generation).toBe(context.generation); - expect(after?.usageMeasuredAtMs).not.toBe(createdAt); - expect(container.schedules.at(-1)?.callback).toBe('billingHeartbeatTick'); - }); -}); - -describe('openIntervalBeforeCreate', () => { - it('opens a fresh generation, records one start, and omits disk metadata', async () => { - const { storage, meter, lifecycle } = setup(); - - const outcome = await lifecycle.openIntervalBeforeCreate(IDENTITY, BILLING_INPUT); - - expect(outcome).toEqual({ kind: 'acked', generation: expect.any(String) }); - const generation = (outcome as { generation: string }).generation; - expect(meter.recordStartInputs).toHaveLength(1); - expect(storage.values.get('container-usage:start-ack-generation:v1')).toBe(generation); - - const context = await getBillingContext(storage); - expect(context?.generation).toBe(generation); - expect(context?.measurementStarted).toBe(false); - expect(context?.metadata).not.toHaveProperty('disk_mb'); - expect(context?.metadata).toMatchObject({ - container_class: 'SandboxVercelSmall', - vcpu: '2', - memory_mib: '4096', - }); - }); - - it('re-acknowledges an existing generation without a second recordStart', async () => { - const { storage, meter, lifecycle } = setup(); - const context = await seedContext(storage); - await storage.put('container-usage:start-ack-generation:v1', context.generation); - - const outcome = await lifecycle.openIntervalBeforeCreate(IDENTITY, BILLING_INPUT); - - expect(outcome).toEqual({ kind: 'acked', generation: context.generation }); - expect(meter.recordStartInputs).toHaveLength(0); - expect((await getBillingContext(storage))?.generation).toBe(context.generation); - }); - - it.each(['insufficient_credits', 'sku_not_found'] as const)( - 'returns definite_rejection for %s and retains the generation', - async code => { - const { storage, meter, lifecycle } = setup(); - meter.startResult = - code === 'insufficient_credits' - ? { - success: false, - error: { - code, - message: 'Insufficient credits', - remainingMicrodollars: 0, - minimumRequiredMicrodollars: 5, - }, - } - : { success: false, error: { code, message: 'SKU is not configured' } }; - - const outcome = await lifecycle.openIntervalBeforeCreate(IDENTITY, BILLING_INPUT); - - expect(outcome.kind).toBe('definite_rejection'); - const generation = (outcome as { generation: string }).generation; - expect((await getBillingContext(storage))?.generation).toBe(generation); - expect(storage.values.get('container-usage:start-ack-generation:v1')).toBeUndefined(); - } - ); - - it.each(['insufficient_credits', 'sku_not_found'] as const)( - 'classifies an existing-generation ack rejection for %s as definite and retains the generation', - async code => { - const { storage, meter, lifecycle } = setup(); - const context = await seedContext(storage); - meter.startResult = - code === 'insufficient_credits' - ? { - success: false, - error: { - code, - message: 'Insufficient credits', - remainingMicrodollars: 0, - minimumRequiredMicrodollars: 5, - }, - } - : { success: false, error: { code, message: 'SKU is not configured' } }; - - const outcome = await lifecycle.openIntervalBeforeCreate(IDENTITY, BILLING_INPUT); - - expect(outcome).toEqual({ - kind: 'definite_rejection', - generation: context.generation, - error: expect.any(Error), - }); - expect((await getBillingContext(storage))?.generation).toBe(context.generation); - } - ); - - it('returns uncertain for a generic meter error and retains the generation', async () => { - const { storage, meter, lifecycle } = setup(); - meter.recordStartBehavior = 'reject'; - - const outcome = await lifecycle.openIntervalBeforeCreate(IDENTITY, BILLING_INPUT); - - expect(outcome.kind).toBe('uncertain'); - const generation = (outcome as { generation: string }).generation; - expect((await getBillingContext(storage))?.generation).toBe(generation); - expect(storage.values.get('container-usage:start-ack-generation:v1')).toBeUndefined(); - }); -}); - -describe('prepareSettlement', () => { - it('persists terminal evidence, marks the continuation due, and never delivers', async () => { - const { storage, meter, billing, schedule } = setup(); - const context = await seedContext(storage); - await saveVercelBillingBinding(storage, bindingFor(context.generation, { createdAtMs: T0 })); - schedule.entries.set(BILLING_HEARTBEAT_CALLBACK, { - dueAtMs: T0 - 1, - payload: context.generation, - }); - - await billing.prepareSettlement({ generation: context.generation, terminalAtMs: T0 + 100_000 }); - - expect((await loadVercelBillingBinding(storage, context.generation))?.terminalAtMs).toBe( - T0 + 100_000 - ); - expect(schedule.calls).toContainEqual({ - op: 'markDue', - callback: BILLING_HEARTBEAT_CALLBACK, - payload: context.generation, - dueAtMs: Date.now(), - }); - expect(meter.recordStopInputs).toHaveLength(0); - expect(meter.recordHeartbeatInputs).toHaveLength(0); - expect((await getBillingContext(storage))?.pendingStop).toBeUndefined(); - }); - - it('never inserts a continuation that was not already armed', async () => { - const { storage, billing, schedule } = setup(); - const context = await seedContext(storage); - await saveVercelBillingBinding(storage, bindingFor(context.generation)); - - await billing.prepareSettlement({ generation: context.generation, terminalAtMs: T0 + 1 }); - - expect(schedule.entries.has(BILLING_HEARTBEAT_CALLBACK)).toBe(false); - }); -}); - -describe('deliverSettlement', () => { - it('pins from createdAt, persists the stop, ticks once, and never uses Date.now() as the end', async () => { - const { storage, meter, billing } = setup(); - const context = await seedContext(storage); - const binding = bindingFor(context.generation, { - createdAtMs: T0, - terminalAtMs: T0 + 100_000, - }); - await saveVercelBillingBinding(storage, binding); - - vi.setSystemTime(T0 + 10_000_000); - await billing.deliverSettlement(context.generation); - - expect(meter.recordStopInputs).toHaveLength(1); - expect(meter.recordStopInputs[0].usageSinceLast).toBe(100); - expect(meter.recordStopInputs[0].service).toBe(SERVICE); - // A successful stop clears the context and closes the generation. - expect(await getBillingContext(storage)).toBeUndefined(); - expect(await loadVercelBillingBinding(storage, context.generation)).toBeUndefined(); - }); - - it('abandons delivery past the 60-minute bound and removes the binding and continuation', async () => { - const { storage, meter, billing, schedule } = setup(); - const context = await seedContext(storage); - const terminalAtMs = T0 + 60_000; - await saveVercelBillingBinding( - storage, - bindingFor(context.generation, { createdAtMs: T0, terminalAtMs }) - ); - schedule.entries.set(BILLING_HEARTBEAT_CALLBACK, { - dueAtMs: T0, - payload: context.generation, - }); - meter.recordStopBehavior = 'reject'; - - await expect(billing.deliverSettlement(context.generation)).rejects.toThrow( - 'meter stop unavailable' - ); - expect(await getBillingContext(storage)).not.toBeUndefined(); - - vi.setSystemTime(terminalAtMs + 60 * 60 * 1_000 + 1); - await billing.deliverSettlement(context.generation); - - expect(await getBillingContext(storage)).toBeUndefined(); - expect(await loadVercelBillingBinding(storage, context.generation)).toBeUndefined(); - expect(schedule.calls).toContainEqual({ - op: 'remove', - callback: BILLING_HEARTBEAT_CALLBACK, - payload: context.generation, - }); - // No create or start is attempted on the abandon path. - expect(meter.recordStartInputs).toHaveLength(0); - }); - - it('fences delivery to its generation and leaves a newer generation untouched', async () => { - const { storage, meter, billing, schedule } = setup(); - const contextB = await seedContext(storage); - const generationA = '00000000-0000-4000-8000-000000000000'; - await saveVercelBillingBinding( - storage, - bindingFor(generationA, { terminalAtMs: T0 + 100_000 }) - ); - - vi.setSystemTime(T0 + 10_000_000); - await billing.deliverSettlement(generationA); - - expect(meter.recordStopInputs).toHaveLength(0); - expect(meter.recordHeartbeatInputs).toHaveLength(0); - expect(schedule.calls).toHaveLength(0); - const after = await getBillingContext(storage); - expect(after?.generation).toBe(contextB.generation); - expect(after?.measurementStarted).toBe(false); - expect(after?.pendingStop).toBeUndefined(); - }); - - it('does not pin without a binding and settles a zero segment from the existing cursor', async () => { - const { storage, meter, billing, lifecycle } = setup(); - const context = await seedContext(storage); - const pin = vi.spyOn(lifecycle, 'pinMeasurementCursor'); - - vi.setSystemTime(T0 + 10_000_000); - await billing.deliverSettlement(context.generation); - - expect(pin).not.toHaveBeenCalled(); - expect(meter.recordStopInputs).toHaveLength(1); - expect(meter.recordStopInputs[0].usageSinceLast).toBe(0); - }); - - it("pins exactly this generation's binding createdAtMs", async () => { - const { storage, meter, billing, lifecycle } = setup(); - const context = await seedContext(storage); - const createdAtMs = T0 - 50_000; - await saveVercelBillingBinding( - storage, - bindingFor(context.generation, { createdAtMs, terminalAtMs: T0 + 50_000 }) - ); - const pin = vi.spyOn(lifecycle, 'pinMeasurementCursor'); - - vi.setSystemTime(T0 + 10_000_000); - await billing.deliverSettlement(context.generation); - - expect(pin).toHaveBeenCalledWith(context.generation, createdAtMs); - expect(meter.recordStopInputs).toHaveLength(1); - expect(meter.recordStopInputs[0].usageSinceLast).toBe(100); - }); -}); - -describe('onGenerationClosed', () => { - it('removes the binding and the continuation and triggers recompose', async () => { - const { storage, billing, schedule } = setup(); - const context = await seedContext(storage); - await saveVercelBillingBinding(storage, bindingFor(context.generation)); - schedule.entries.set(BILLING_HEARTBEAT_CALLBACK, { - dueAtMs: T0, - payload: context.generation, - }); - - await billing.onGenerationClosed(context); - - expect(await loadVercelBillingBinding(storage, context.generation)).toBeUndefined(); - expect(schedule.entries.has(BILLING_HEARTBEAT_CALLBACK)).toBe(false); - expect(schedule.calls).toContainEqual({ - op: 'remove', - callback: BILLING_HEARTBEAT_CALLBACK, - payload: context.generation, - }); - }); -}); - -describe('createVercelBillingRuntime', () => { - const classes = Object.entries(VERCEL_BILLING_CAPACITIES); - const [classA, capacityA] = classes[0]!; - - function resourcesFor(capacity: { vcpu: number; memoryMiB: number }): VercelSandboxResources { - return { - vcpus: capacity.vcpu, - memory: capacity.memoryMiB, - } as unknown as VercelSandboxResources; - } - - function helperSchedule() { - const due: Array<{ callback: string; payload?: unknown }> = []; - const calls: string[] = []; - return { - due, - calls, - async dueEntries() { - const entries = [...due]; - due.length = 0; - return entries; - }, - async completeDue(entry: { callback: string }) { - calls.push(`complete:${entry.callback}`); - }, - async ensure(callback: string) { - calls.push(`ensure:${callback}`); - }, - async schedule(callback: string) { - calls.push(`schedule:${callback}`); - }, - async remove(callback: string) { - calls.push(`remove:${callback}`); - }, - async deferRetry(callback: string) { - calls.push(`defer:${callback}`); - }, - async markDue() {}, - }; - } - - function harness(options: { requestStop?: () => Promise } = {}) { - const { storage, meter, pending, heartbeat } = setup(); - const schedule = helperSchedule(); - let resources: VercelSandboxResources | undefined; - let allocation = { kind: 'stopped', allocationId: 'a1', providerRef: 'provider-ref-1' }; - const stopRef = vi.fn(async () => false); - const failUnconfirmedCleanup = vi.fn(async () => undefined); - const requestStop = vi.fn(options.requestStop ?? (async () => undefined)); - const runtime = createVercelBillingRuntime({ - storage: storage as unknown as DurableObjectStorage, - env: () => ({ CONTAINER_USAGE_METER: meter }) as never, - sandboxId: 'ses-abcdef', - schedule: schedule as unknown as BillingScheduleTable, - currentProvider: () => 'vercel', - currentPin: () => ({ - provider: 'vercel', - allocationName: 'ses-abcdef', - configuration: - resources === undefined ? null : { provider: 'vercel', resources: resources }, - locator: null, - billing: null, - containment: null, - }), - readAllocation: async () => allocation as unknown as AllocationState, - requireOwner: async () => 'user-1', - isCreateInFlight: () => false, - provider: () => - ({ - observe: async () => ({ status: 'unknown' }), - }) as unknown as ProviderAdapter, - timers: () => - ({ providerStopAttemptMs: 1_000, providerCreateMs: 1_000 }) as unknown as SandboxTimers, - stopRef, - failUnconfirmedCleanup, - requestStop, - waitUntil: promise => { - pending.push(promise); - }, - }); - return { - runtime, - storage, - schedule, - heartbeat, - pending, - stopRef, - failUnconfirmedCleanup, - requestStop, - setResources: (value: VercelSandboxResources | undefined) => { - resources = value; - }, - setAllocation: (value: unknown) => { - allocation = value as typeof allocation; - }, - }; - } - - it('restores from the stored context service and only rebuilds on a resource change after the context closes', async () => { - const h = harness(); - await seedContext(h.storage); - - // Evicted state: resources missing, so the runtime is rebuilt from the - // stored context service (identity unknown rather than a class). - const first = await h.runtime.ensureRuntime(); - expect(first).toBeDefined(); - expect(first?.identity).toBeUndefined(); - - // A resource change while the context is still open must keep the cached - // runtime: the cached object still describes the open generation. - h.setResources(resourcesFor(capacityA)); - const cached = await h.runtime.ensureRuntime(); - expect(cached).toBe(first); - expect(cached?.identity).toBeUndefined(); - - // Only once the context is closed does the changed resource class rebuild. - await clearBillingContext(h.storage); - const rebuilt = await h.runtime.ensureRuntime(); - expect(rebuilt).not.toBe(first); - expect(rebuilt?.identity?.sandboxClassName).toBe(classA); - }); - - it('schedules an unconfirmed settlement, then binds the stored terminal time when confirmed', async () => { - const h = harness(); - const context = await seedContext(h.storage, { usageMeasuredAtMs: T0 + 500 }); - await saveVercelBillingBinding(h.storage, bindingFor(context.generation)); - h.setAllocation({ kind: 'stopped', allocationId: 'a1', providerRef: 'provider-ref-1' }); - - await h.runtime.settleStoppedCreatedRef('provider-ref-1', false); - expect(h.schedule.calls).toContain(`schedule:${VERCEL_BILLING_SETTLEMENT_CALLBACK}`); - expect((await loadVercelBillingBinding(h.storage, context.generation))?.terminalAtMs).toBe( - undefined - ); - - await h.runtime.settleStoppedCreatedRef('provider-ref-1', true); - // The stored measurement time is used, not the current clock. - expect((await loadVercelBillingBinding(h.storage, context.generation))?.terminalAtMs).toBe( - T0 + 500 - ); - }); - - it('keeps a stored terminal time instead of a newer measurement', async () => { - const h = harness(); - const context = await seedContext(h.storage, { usageMeasuredAtMs: T0 + 5_000 }); - await saveVercelBillingBinding(h.storage, bindingFor(context.generation, { terminalAtMs: T0 })); - h.setAllocation({ kind: 'stopped', allocationId: 'a1', providerRef: 'provider-ref-1' }); - const active = await h.runtime.ensureRuntime(); - if (active === undefined) throw new Error('missing runtime'); - const persistStop = vi.spyOn(active.heartbeat, 'persistStop'); - - await h.runtime.settleStoppedCreatedRef('provider-ref-1', true); - - expect(persistStop).toHaveBeenCalledWith( - expect.objectContaining({ reason: 'runtime_signal' }), - T0 - ); - expect((await loadVercelBillingBinding(h.storage, context.generation))?.terminalAtMs).toBe(T0); - }); - - it('fails unconfirmed cleanup only once the create deadline has passed', async () => { - const h = harness(); - const context = await seedContext(h.storage, { startEpochMs: T0 - 500 }); - await saveVercelBillingBinding(h.storage, bindingFor(context.generation)); - h.setAllocation({ kind: 'creating', allocationId: 'a1', providerRef: 'provider-ref-1' }); - - h.schedule.due.push({ - callback: VERCEL_BILLING_SETTLEMENT_CALLBACK, - payload: context.generation, - }); - await h.runtime.runAlarm(); - expect(h.stopRef).toHaveBeenCalledWith('provider-ref-1', 'a1'); - expect(h.failUnconfirmedCleanup).not.toHaveBeenCalled(); - expect(h.schedule.calls).toContain(`schedule:${VERCEL_BILLING_SETTLEMENT_CALLBACK}`); - - // Past the create deadline the unconfirmed stop fails cleanup closed. - vi.setSystemTime(T0 + 600); - h.schedule.due.push({ - callback: VERCEL_BILLING_SETTLEMENT_CALLBACK, - payload: context.generation, - }); - await h.runtime.runAlarm(); - expect(h.failUnconfirmedCleanup).toHaveBeenCalledWith( - expect.objectContaining({ kind: 'creating' }) - ); - }); - - it('fails unconfirmed cleanup after the deadline and drops due entries for stale generations', async () => { - const h = harness(); - const context = await seedContext(h.storage, { startEpochMs: T0 - 600_000 }); - await saveVercelBillingBinding(h.storage, bindingFor(context.generation)); - h.setAllocation({ kind: 'creating', allocationId: 'a1', providerRef: 'provider-ref-1' }); - - h.schedule.due.push({ - callback: VERCEL_BILLING_SETTLEMENT_CALLBACK, - payload: context.generation, - }); - await h.runtime.runAlarm(); - expect(h.stopRef).toHaveBeenCalledWith('provider-ref-1', 'a1'); - expect(h.failUnconfirmedCleanup).toHaveBeenCalledWith( - expect.objectContaining({ kind: 'creating' }) - ); - expect(h.schedule.calls).toContain(`schedule:${VERCEL_BILLING_SETTLEMENT_CALLBACK}`); - - // A due entry for another generation is dropped and its binding removed - // without provider work. - h.schedule.due.push({ - callback: VERCEL_BILLING_SETTLEMENT_CALLBACK, - payload: 'stale-generation', - }); - await h.runtime.runAlarm(); - expect(h.schedule.calls).toContain(`complete:${VERCEL_BILLING_SETTLEMENT_CALLBACK}`); - expect(h.stopRef).toHaveBeenCalledTimes(1); - }); - - it('does not await the stop request, so the billing alarm path returns without self-waiting', async () => { - vi.useRealTimers(); - const never = new Promise(() => undefined); - const h = harness({ requestStop: () => never }); - const context = await seedContext(h.storage); - const active = await h.runtime.ensureRuntime(); - if (active === undefined) throw new Error('missing runtime'); - - const outcome = await Promise.race([ - active.lifecycle - .enforceBudgetStop( - { sandboxClassName: classA } as unknown as BillingIdentity, - { verdict: 'stop' }, - { generation: context.generation, startEpochMs: context.startEpochMs } - ) - .then(() => 'returned' as const), - new Promise<'timeout'>(resolve => setTimeout(() => resolve('timeout'), 250)), - ]); - - expect(outcome).toBe('returned'); - expect(h.requestStop).toHaveBeenCalledTimes(1); - expect(h.pending).toHaveLength(1); - }); -}); diff --git a/services/cloud-agent-next/src/sandbox-control/vercel-billing.ts b/services/cloud-agent-next/src/sandbox-control/vercel-billing.ts deleted file mode 100644 index 5149ab14b7d..00000000000 --- a/services/cloud-agent-next/src/sandbox-control/vercel-billing.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * The Vercel billing producer. It owns the generation → provider-lifetime - * binding and splits settlement into a synchronous, awaited "prepare" phase - * (persist terminal evidence, mark the existing continuation due) and a later - * "deliver" phase (pin the alive cursor, persist the stop, tick the heartbeat). - * - * It never calls `setAlarm`/`deleteAlarm` (that stays in `control-alarm.ts`) and never - * calls `recordStop` directly; settlement delivery routes through - * `billingHeartbeatTick`, which owns stop retry and the 60-minute abandon. - */ -import { - BILLING_HEARTBEAT_CALLBACK, - getBillingContext, - type BillingContext, - type BillingContextStorage, - type BillingHeartbeatController, -} from '@kilocode/container-usage'; -import { z } from 'zod'; -import type { MeteredBillingLifecycle } from '../metered-billing-lifecycle.js'; -import type { BillingScheduleTable } from './billing-schedule.js'; - -export const VERCEL_BILLING_BINDING_KEY_PREFIX = 'vercel-billing:binding:v1:'; - -/** - * Continuation callback id; the payload is the generation id. It is the - * installed heartbeat's callback, so one schedule entry per open generation is - * shared by the measurement heartbeat and settlement. Cancellation must not drop - * the durable continuation before settlement completes. - */ -export const VERCEL_BILLING_SETTLEMENT_CALLBACK = BILLING_HEARTBEAT_CALLBACK; - -export const vercelBillingBindingSchema = z - .object({ - generation: z.uuid(), - providerRef: z.string().min(1).max(512), - createdAtMs: z.number().int().nonnegative().finite(), - terminalAtMs: z.number().int().nonnegative().finite().optional(), - }) - .strict(); - -export type VercelBillingBinding = z.infer; - -export function vercelBillingBindingKey(generation: string): string { - return `${VERCEL_BILLING_BINDING_KEY_PREFIX}${generation}`; -} - -export async function loadVercelBillingBinding( - storage: BillingContextStorage, - generation: string -): Promise { - const stored = await storage.get(vercelBillingBindingKey(generation)); - if (stored === undefined) return undefined; - return vercelBillingBindingSchema.parse(stored); -} - -export async function saveVercelBillingBinding( - storage: BillingContextStorage, - binding: VercelBillingBinding -): Promise { - await storage.put( - vercelBillingBindingKey(binding.generation), - vercelBillingBindingSchema.parse(binding) - ); -} - -export async function deleteVercelBillingBinding( - storage: BillingContextStorage, - generation: string -): Promise { - await storage.delete(vercelBillingBindingKey(generation)); -} - -export type VercelBillingDeps = { - storage: BillingContextStorage; - lifecycle: MeteredBillingLifecycle; - heartbeat: BillingHeartbeatController; - schedule: BillingScheduleTable; -}; - -export type VercelSettlementEvidence = { - generation: string; - createdAtMs?: number; - terminalAtMs?: number; -}; - -export class VercelBilling { - constructor(private readonly deps: VercelBillingDeps) {} - - /** - * Persist terminal evidence and mark the existing continuation settlement-due. - * It must already be armed by the pre-create path; this never inserts one. - * Does not take the billing or heartbeat queue and does not call the pin, - * `persistStop`, or the heartbeat tick. A compose error propagates. - */ - async prepareSettlement(evidence: VercelSettlementEvidence): Promise { - const existing = await loadVercelBillingBinding(this.deps.storage, evidence.generation); - if (existing) { - const merged: VercelBillingBinding = { - ...existing, - ...(evidence.createdAtMs !== undefined ? { createdAtMs: evidence.createdAtMs } : {}), - ...(evidence.terminalAtMs !== undefined ? { terminalAtMs: evidence.terminalAtMs } : {}), - }; - if ( - merged.createdAtMs !== existing.createdAtMs || - merged.terminalAtMs !== existing.terminalAtMs - ) { - await saveVercelBillingBinding(this.deps.storage, merged); - } - } - await this.deps.schedule.markDue( - VERCEL_BILLING_SETTLEMENT_CALLBACK, - evidence.generation, - Date.now() - ); - } - - /** - * Runs only after `prepareSettlement` returned. Pins the alive cursor when a - * real lifetime has not been measured, ends settlement at the stored terminal - * time (never `Date.now()`), then delivers through the heartbeat tick. - * - * The delivery is fenced to its generation: a delayed delivery for an older - * generation must not stop a newer one. The pin is taken only when this - * generation's binding supplies the create-response lifetime. - */ - async deliverSettlement(generation: string): Promise { - let context = await getBillingContext(this.deps.storage); - if (!context || context.generation !== generation) return; - const binding = await loadVercelBillingBinding(this.deps.storage, generation); - if (!context.measurementStarted && binding !== undefined && binding.generation === generation) { - await this.deps.lifecycle.pinMeasurementCursor(generation, binding.createdAtMs); - const pinned = await getBillingContext(this.deps.storage); - if (!pinned || pinned.generation !== generation) return; - context = pinned; - } - const end = binding?.terminalAtMs ?? context.usageMeasuredAtMs; - await this.deps.heartbeat.persistStop({ reason: 'runtime_signal' }, end); - await this.deps.heartbeat.billingHeartbeatTick(generation); - } - - /** - * Generation-close callback. Removes the binding and the continuation; `remove` - * triggers the compose pass. It does not take the billing or heartbeat queue - * and does not create anything. - */ - async onGenerationClosed(context: BillingContext): Promise { - await deleteVercelBillingBinding(this.deps.storage, context.generation); - await this.deps.schedule.remove(VERCEL_BILLING_SETTLEMENT_CALLBACK, context.generation); - } -} diff --git a/services/cloud-agent-next/src/sandbox-control/vercel-provider.ts b/services/cloud-agent-next/src/sandbox-control/vercel-provider.ts index d804800e484..32fa08df8f6 100644 --- a/services/cloud-agent-next/src/sandbox-control/vercel-provider.ts +++ b/services/cloud-agent-next/src/sandbox-control/vercel-provider.ts @@ -97,16 +97,6 @@ function encodeVercelSessionProviderRef( }); } -/** - * Lifetime evidence the adapter writes to the current billing generation's - * binding. The adapter does not know the generation; the sink resolves it. - */ -export type VercelBillingLifetimeSink = (evidence: { - providerRef: string; - createdAtMs?: number; - terminalAtMs?: number; -}) => Promise; - export function decodeVercelProviderRef(raw: string | null): VercelProviderRef | null { if (raw === null) return null; try { @@ -132,7 +122,6 @@ export function createVercelProviderAdapter(deps: { config?: VercelSandboxRuntimeConfig; restClient?: VercelControlRestClient; now?: () => number; - billingLifetimeSink?: VercelBillingLifetimeSink; /** * Authoritative contained grants for this sandbox, read at create and on * every credential refresh. Required: a Vercel sandbox must never open @@ -163,6 +152,7 @@ export function createVercelProviderAdapter(deps: { new VercelSandboxRestClient({ accessToken: config.accessToken, teamId: config.teamId, + scope: config.scope, projectId: config.projectId, fetch, }); @@ -182,18 +172,6 @@ export function createVercelProviderAdapter(deps: { ); } }; - const recordBillingLifetime = async ( - providerRef: string, - session: VercelSandboxSession - ): Promise => { - if (!deps.billingLifetimeSink) return; - const terminalAtMs = session.stoppedAt ?? session.abortedAt; - await deps.billingLifetimeSink({ - providerRef, - createdAtMs: session.createdAt, - ...(terminalAtMs === undefined ? {} : { terminalAtMs }), - }); - }; return { ensureBillingAdmission, @@ -226,7 +204,7 @@ export function createVercelProviderAdapter(deps: { name: intent.allocationName ?? deps.sandboxName, operationId: intent.intentId, runtimeBuildId: config.runtimeBuildId, - snapshotId: config.snapshotId, + source: { type: 'snapshot', snapshotId: config.snapshotId }, runtime: config.runtime, timeoutMs: config.initialTimeoutMs, ...(config.resources === undefined ? {} : { resources: config.resources }), @@ -242,7 +220,6 @@ export function createVercelProviderAdapter(deps: { throw error; }); const providerRef = encodeVercelSessionProviderRef(created.runtime, created.session); - await recordBillingLifetime(providerRef, created.session); return { providerRef }; }, async launch(ref, { command, env }) { @@ -259,7 +236,7 @@ export function createVercelProviderAdapter(deps: { PROVIDER_INSTANCE_ID: ref, WRAPPER_LOG_PATH: CONTROL_WRAPPER_LOG_PATH, }, - sudo: false, + sudo: command.user === 'root', wait: false, }); return { startSource: 'image' }; @@ -269,7 +246,6 @@ export function createVercelProviderAdapter(deps: { try { if (parsed) { const { session } = await restClient.getSession(parsed.sessionId, parsed.sandboxName); - await recordBillingLifetime(ref ?? encodeVercelProviderRef(parsed), session); return { status: classifyVercelSessionStatus(session.status) }; } if (ref !== null || !intent) return { status: 'unknown' }; @@ -287,7 +263,6 @@ export function createVercelProviderAdapter(deps: { }; } const providerRef = encodeVercelSessionProviderRef(inspected.runtime, inspected.session); - await recordBillingLifetime(providerRef, inspected.session); return { status: classifyVercelSessionStatus(inspected.session.status), providerRef, @@ -312,7 +287,6 @@ export function createVercelProviderAdapter(deps: { logControlDiagnostic('native_stop', { ...diagnostic, result: 'started' }); try { const session = await restClient.stopSession(parsed.sessionId, parsed.sandboxName); - await recordBillingLifetime(ref ?? encodeVercelProviderRef(parsed), session); const result = TERMINAL_STATUSES.has(session.status) ? 'terminal' : 'retryable'; logControlDiagnostic('native_stop', { ...diagnostic, diff --git a/services/cloud-agent-next/src/sandbox-id.test.ts b/services/cloud-agent-next/src/sandbox-id.test.ts index 1ef8f9135bb..9de0438f422 100644 --- a/services/cloud-agent-next/src/sandbox-id.test.ts +++ b/services/cloud-agent-next/src/sandbox-id.test.ts @@ -522,15 +522,16 @@ describe('generateSandboxId', () => { describe('selectSandboxForNewSession', () => { const completeVercelConfiguration = { - VERCEL_SANDBOX_ORG_IDS: 'org-id', - VERCEL_TOKEN: 'token', - VERCEL_TEAM_ID: 'team-id', - VERCEL_PROJECT_ID: 'project-id', - VERCEL_SANDBOX_SNAPSHOT_ID: 'snapshot-id', - VERCEL_SANDBOX_RUNTIME_BUILD_ID: 'build-id', - VERCEL_SANDBOX_RUNTIME: 'node24', - VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: '300000', - VERCEL_SANDBOX_EXTEND_DURATION_MS: '600000', + CREDENTIAL_CONTAINMENT_ENABLED: undefined, + ...{ VERCEL_SANDBOX_ORG_IDS: 'org-id' }, + ...{ VERCEL_TOKEN: 'token' }, + ...{ VERCEL_TEAM_ID: 'team-id' }, + ...{ VERCEL_PROJECT_ID: 'project-id' }, + ...{ VERCEL_SANDBOX_SNAPSHOT_ID: 'snapshot-id' }, + ...{ VERCEL_SANDBOX_RUNTIME_BUILD_ID: 'build-id' }, + ...{ VERCEL_SANDBOX_RUNTIME: 'node24' }, + ...{ VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: '300000' }, + ...{ VERCEL_SANDBOX_EXTEND_DURATION_MS: '600000' }, }; const controlSessionId = 'workspace_12345678-1234-1234-1234-123456789abc'; const otherSessionId = 'workspace_aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; @@ -543,7 +544,7 @@ describe('selectSandboxForNewSession', () => { env: { ...completeVercelConfiguration, PER_SESSION_SANDBOX_ORG_IDS: rollout, - VERCEL_SANDBOX_ORG_IDS: rollout, + ...{ VERCEL_SANDBOX_ORG_IDS: rollout }, }, orgId: 'org-id', userId: 'user-id', @@ -614,7 +615,7 @@ describe('selectSandboxForNewSession', () => { } ); - it('selects Vercel for an enabled, allowlisted, isolated control-plane session', async () => { + it('ignores retired Vercel platform enrollment for an isolated control-plane session', async () => { const selection = await selectSandboxForNewSession({ env: completeVercelConfiguration, orgId: 'org-id', @@ -622,7 +623,7 @@ describe('selectSandboxForNewSession', () => { sessionId: controlSessionId, }); - expect(selection.provider).toBe('vercel'); + expect(selection.provider).toBe('cloudflare-containers'); expect(selection.sandboxId).toMatch(/^ses-/); }); @@ -661,7 +662,7 @@ describe('selectSandboxForNewSession', () => { env: { PER_SESSION_SANDBOX_ORG_IDS: 'org-id', ...completeVercelConfiguration, - VERCEL_SANDBOX_ORG_IDS: 'other-org', + ...{ VERCEL_SANDBOX_ORG_IDS: 'other-org' }, }, orgId: 'org-id', userId: 'user-id', @@ -672,19 +673,19 @@ describe('selectSandboxForNewSession', () => { expect(selection.sandboxId).toMatch(/^ses-/); }); - it('selects Vercel for any isolated control-plane organization when enrollment is wildcarded', async () => { + it('ignores the retired wildcard for isolated organizations', async () => { const selection = await selectSandboxForNewSession({ env: { PER_SESSION_SANDBOX_ORG_IDS: '*', ...completeVercelConfiguration, - VERCEL_SANDBOX_ORG_IDS: '*', + ...{ VERCEL_SANDBOX_ORG_IDS: '*' }, }, orgId: 'org-id', userId: 'user-id', sessionId: controlSessionId, }); - expect(selection.provider).toBe('vercel'); + expect(selection.provider).toBe('cloudflare-containers'); expect(selection.sandboxId).toMatch(/^ses-/); }); @@ -693,13 +694,13 @@ describe('selectSandboxForNewSession', () => { env: { PER_SESSION_SANDBOX_ORG_IDS: '*', ...completeVercelConfiguration, - VERCEL_SANDBOX_ORG_IDS: '*', + ...{ VERCEL_SANDBOX_ORG_IDS: '*' }, }, userId: 'user-id', sessionId: controlSessionId, }); - expect(selection.provider).toBe('vercel'); + expect(selection.provider).toBe('cloudflare-containers'); expect(selection.sandboxId).toMatch(/^ses-/); }); @@ -708,7 +709,7 @@ describe('selectSandboxForNewSession', () => { env: { PER_SESSION_SANDBOX_ORG_IDS: 'org-id', ...completeVercelConfiguration, - VERCEL_SANDBOX_RUNTIME: 'node22', + ...{ VERCEL_SANDBOX_RUNTIME: 'node22' }, }, orgId: 'org-id', userId: 'user-id', @@ -773,12 +774,12 @@ describe('selectSandboxForNewSession', () => { expect(selection.sandboxId).toMatch(/^org-/); }); - it('keeps Vercel precedence when both enrollments are enabled', async () => { + it('uses Cloudflare containers when retired Vercel enrollment is also present', async () => { const selection = await selectSandboxForNewSession({ env: { PER_SESSION_SANDBOX_ORG_IDS: '*', ...completeVercelConfiguration, - VERCEL_SANDBOX_ORG_IDS: '*', + ...{ VERCEL_SANDBOX_ORG_IDS: '*' }, CLOUDFLARE_CONTAINERS_ORG_IDS: '*', }, orgId: 'org-id', @@ -786,7 +787,7 @@ describe('selectSandboxForNewSession', () => { sessionId: controlSessionId, }); - expect(selection.provider).toBe('vercel'); + expect(selection.provider).toBe('cloudflare-containers'); expect(selection.sandboxId).toMatch(/^ses-/); }); @@ -795,7 +796,7 @@ describe('selectSandboxForNewSession', () => { env: { PER_SESSION_SANDBOX_ORG_IDS: '*', ...completeVercelConfiguration, - VERCEL_SANDBOX_ORG_IDS: '*', + ...{ VERCEL_SANDBOX_ORG_IDS: '*' }, CLOUDFLARE_CONTAINERS_ORG_IDS: '*', CLOUD_AGENT_CONTAINER_BILLING_ENABLED: 'true', CLOUD_AGENT_CONTAINER_BILLING_ORG_IDS: 'org-id', @@ -814,7 +815,7 @@ describe('selectSandboxForNewSession', () => { env: { PER_SESSION_SANDBOX_ORG_IDS: '*', ...completeVercelConfiguration, - VERCEL_SANDBOX_ORG_IDS: '*', + ...{ VERCEL_SANDBOX_ORG_IDS: '*' }, CLOUD_AGENT_CONTAINER_BILLING_ENABLED: 'true', CLOUD_AGENT_CONTAINER_BILLING_ORG_IDS: 'org-id', }, @@ -827,12 +828,12 @@ describe('selectSandboxForNewSession', () => { expect(selection.sandboxId).toMatch(/^ses-/); }); - it('still selects Vercel for a dual-enrolled organization when enforcement is off', async () => { + it('uses Cloudflare containers when enforcement is off', async () => { const selection = await selectSandboxForNewSession({ env: { PER_SESSION_SANDBOX_ORG_IDS: '*', ...completeVercelConfiguration, - VERCEL_SANDBOX_ORG_IDS: '*', + ...{ VERCEL_SANDBOX_ORG_IDS: '*' }, CLOUDFLARE_CONTAINERS_ORG_IDS: '*', CLOUD_AGENT_CONTAINER_BILLING_ENABLED: 'false', }, @@ -841,7 +842,7 @@ describe('selectSandboxForNewSession', () => { sessionId: controlSessionId, }); - expect(selection.provider).toBe('vercel'); + expect(selection.provider).toBe('cloudflare-containers'); }); it('skips Vercel for a personal owner enforced through the user allowlist', async () => { @@ -849,7 +850,7 @@ describe('selectSandboxForNewSession', () => { env: { PER_SESSION_SANDBOX_ORG_IDS: '*', ...completeVercelConfiguration, - VERCEL_SANDBOX_ORG_IDS: '*', + ...{ VERCEL_SANDBOX_ORG_IDS: '*' }, CLOUDFLARE_CONTAINERS_ORG_IDS: '*', CLOUD_AGENT_CONTAINER_BILLING_ENABLED: 'true', CLOUD_AGENT_CONTAINER_BILLING_USER_IDS: 'user-id', @@ -866,7 +867,7 @@ describe('selectSandboxForNewSession', () => { { PER_SESSION_SANDBOX_ORG_IDS: '*', ...completeVercelConfiguration, - VERCEL_SANDBOX_ORG_IDS: '*', + ...{ VERCEL_SANDBOX_ORG_IDS: '*' }, CLOUDFLARE_CONTAINERS_ORG_IDS: '*', CLOUD_AGENT_CONTAINER_BILLING_ENABLED: 'true', CLOUD_AGENT_CONTAINER_BILLING_ORG_IDS: 'org-id', diff --git a/services/cloud-agent-next/src/sandbox-id.ts b/services/cloud-agent-next/src/sandbox-id.ts index 44364f39e9d..ac427c5bbb4 100644 --- a/services/cloud-agent-next/src/sandbox-id.ts +++ b/services/cloud-agent-next/src/sandbox-id.ts @@ -1,3 +1,4 @@ +import { enrolledByocVercelOwner } from './byoc/vercel-owner.js'; import { CLOUDFLARE_CONTAINERS_DEFAULT_ALLOCATION, CLOUDFLARE_SANDBOX_SMALL_NATIVE_ALLOCATION, @@ -10,12 +11,6 @@ import { providerUsesOutboundCredentialProxy } from './agent-sandbox/capabilitie import type { AgentSandboxProvider, SandboxId, Env } from './types.js'; import { CODE_REVIEW_PLATFORM } from './session-identity.js'; import type { Sandbox } from '@cloudflare/sandbox'; -import { - parseVercelSandboxEnrollment, - parseVercelSandboxRuntimeConfig, - type VercelSandboxEnrollmentEnv, - type VercelSandboxRuntimeConfigEnv, -} from './agent-sandbox/vercel/vercel-runtime-config.js'; import { isCloudflareContainersEnrolled, type CloudflareContainersEnrollmentEnv, @@ -232,9 +227,8 @@ export type SandboxSelection = { export type SandboxSelectionEnv = { PER_SESSION_SANDBOX_ORG_IDS?: string; -} & VercelSandboxEnrollmentEnv & - VercelSandboxRuntimeConfigEnv & - CloudflareContainersEnrollmentEnv & + BYOC_VERCEL_IDS?: string; +} & CloudflareContainersEnrollmentEnv & Pick< Env, | 'CLOUD_AGENT_CONTAINER_BILLING_ENABLED' @@ -253,10 +247,8 @@ type SelectSandboxForNewSessionInput = { }; /** - * Resolves the sandbox provider for an already-computed sandbox ID. Vercel - * is call-home only: isolated `workspace_` sessions whose owner is on - * `VERCEL_SANDBOX_ORG_IDS` (`*` includes personal) with complete operational - * configuration. Other owners use Cloudflare. + * Resolves the platform provider or an explicitly selected allocation. + * Vercel admission belongs to BYOC registration; platform defaults use Cloudflare. */ export function selectSandboxProvider(input: { env: SandboxSelectionEnv; @@ -294,21 +286,6 @@ export function selectDefaultSandboxProvider(input: { const eligible = (provider: AgentSandboxProvider): boolean => !enforced || providerSupportsEnforcedBilling(provider); - const enrollment = parseVercelSandboxEnrollment(input.env); - const runtimeConfig = parseVercelSandboxRuntimeConfig(input.env); - const enrolled = - input.orgId !== undefined - ? enrollment.orgIds.has('*') || enrollment.orgIds.has(input.orgId) - : enrollment.allowPersonal; - const useVercel = - eligible('vercel') && - input.isolated && - enrollment.enabled && - enrolled && - runtimeConfig !== undefined; - - if (useVercel) return 'vercel'; - const useContainers = eligible('cloudflare-containers') && input.isolated && @@ -384,15 +361,15 @@ export function getDefaultSandboxDestination( env: SandboxSelectionEnv, owner: { userId: string; orgId?: string } ): SandboxDestination { + if (enrolledByocVercelOwner(env, owner) !== undefined) { + return { provider: { id: 'vercel', account: 'byoc' }, instanceType: 'small' }; + } const provider = selectDefaultSandboxProvider({ env, orgId: owner.orgId, userId: owner.userId, isolated: true, }); - if (provider === 'vercel') { - return { provider: { id: 'vercel', account: 'kilo' }, instanceType: 'default' }; - } if (provider === 'cloudflare-containers') { return getSandboxAllocationRequest(CLOUDFLARE_CONTAINERS_DEFAULT_ALLOCATION); } diff --git a/services/cloud-agent-next/src/sandbox-provider-binding.test.ts b/services/cloud-agent-next/src/sandbox-provider-binding.test.ts new file mode 100644 index 00000000000..a7b9052bba3 --- /dev/null +++ b/services/cloud-agent-next/src/sandbox-provider-binding.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest'; +import { + bindingFromLegacyProvider, + providerKindFromBinding, + sameSandboxProviderBinding, + SandboxProviderBindingSchema, +} from './sandbox-provider-binding.js'; + +describe('SandboxProviderBinding', () => { + it('accepts each closed provider source and preserves its identity', () => { + const bindings = [ + { kind: 'cloudflare' }, + { kind: 'vercel', source: { kind: 'platform' } }, + { + kind: 'vercel', + source: { + kind: 'byoc', + organizationId: 'org-1', + credentialId: 'credential-1', + }, + }, + { + kind: 'vercel', + source: { + kind: 'byoc', + userId: 'oauth/user-1', + credentialId: 'credential-1', + }, + }, + ] as const; + + for (const binding of bindings) { + expect(SandboxProviderBindingSchema.parse(binding)).toEqual(binding); + expect(providerKindFromBinding(binding)).toBe(binding.kind); + } + }); + + it('maps legacy provider strings only to platform sources', () => { + expect(bindingFromLegacyProvider('cloudflare')).toEqual({ kind: 'cloudflare' }); + expect(bindingFromLegacyProvider('vercel')).toEqual({ + kind: 'vercel', + source: { kind: 'platform' }, + }); + }); + + it('pins BYOC organization and credential identity', () => { + const binding = { + kind: 'vercel' as const, + source: { kind: 'byoc' as const, organizationId: 'org-1', credentialId: 'credential-1' }, + }; + expect(sameSandboxProviderBinding(binding, { ...binding })).toBe(true); + expect( + sameSandboxProviderBinding(binding, { + ...binding, + source: { ...binding.source, credentialId: 'credential-2' }, + }) + ).toBe(false); + expect( + SandboxProviderBindingSchema.safeParse({ kind: 'vercel', source: { kind: 'byoc' } }).success + ).toBe(false); + }); + + it('pins BYOC personal user and credential identity', () => { + const binding = { + kind: 'vercel' as const, + source: { kind: 'byoc' as const, userId: 'oauth/user-1', credentialId: 'credential-1' }, + }; + expect(sameSandboxProviderBinding(binding, { ...binding })).toBe(true); + expect( + sameSandboxProviderBinding(binding, { + ...binding, + source: { ...binding.source, userId: 'oauth/user-2' }, + }) + ).toBe(false); + }); +}); diff --git a/services/cloud-agent-next/src/sandbox-provider-binding.ts b/services/cloud-agent-next/src/sandbox-provider-binding.ts new file mode 100644 index 00000000000..878af8a1cb4 --- /dev/null +++ b/services/cloud-agent-next/src/sandbox-provider-binding.ts @@ -0,0 +1,69 @@ +import type { ByocVercelOwner } from './byoc/vercel-owner.js'; +import { z } from 'zod'; + +import type { AgentSandboxProvider } from './types.js'; + +export type SandboxProviderBinding = + | { kind: 'cloudflare' } + | { kind: 'cloudflare-containers' } + // Decode-only legacy binding. Runtime admission rejects retired platform Vercel. + | { kind: 'vercel'; source: { kind: 'platform' } } + | { kind: 'vercel'; source: { kind: 'byoc'; credentialId: string } & ByocVercelOwner }; + +export const SandboxProviderBindingSchema: z.ZodType = z.discriminatedUnion( + 'kind', + [ + z.object({ kind: z.literal('cloudflare') }).strict(), + z.object({ kind: z.literal('cloudflare-containers') }).strict(), + z + .object({ + kind: z.literal('vercel'), + source: z.union([ + z.object({ kind: z.literal('platform') }).strict(), + z + .object({ + kind: z.literal('byoc'), + organizationId: z.string().min(1), + credentialId: z.string().min(1), + }) + .strict(), + z + .object({ + kind: z.literal('byoc'), + userId: z.string().min(1), + credentialId: z.string().min(1), + }) + .strict(), + ]), + }) + .strict(), + ] +) as z.ZodType; + +export function bindingFromLegacyProvider(provider: AgentSandboxProvider): SandboxProviderBinding { + return provider === 'vercel' + ? { kind: 'vercel', source: { kind: 'platform' } } + : { kind: provider }; +} + +export function providerKindFromBinding(binding: SandboxProviderBinding): AgentSandboxProvider { + return binding.kind; +} + +export function sameSandboxProviderBinding( + left: SandboxProviderBinding, + right: SandboxProviderBinding +): boolean { + if (left.kind !== right.kind) return false; + if (left.kind !== 'vercel' || right.kind !== 'vercel') return true; + if (left.source.kind !== right.source.kind) return false; + if (left.source.kind !== 'byoc' || right.source.kind !== 'byoc') return true; + if (left.source.credentialId !== right.source.credentialId) return false; + if (left.source.organizationId !== undefined && right.source.organizationId !== undefined) { + return left.source.organizationId === right.source.organizationId; + } + if (left.source.userId !== undefined && right.source.userId !== undefined) { + return left.source.userId === right.source.userId; + } + return false; +} diff --git a/services/cloud-agent-next/src/sandbox-selection.test.ts b/services/cloud-agent-next/src/sandbox-selection.test.ts index b2e3d4dec40..7a3b3df30cc 100644 --- a/services/cloud-agent-next/src/sandbox-selection.test.ts +++ b/services/cloud-agent-next/src/sandbox-selection.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { - getKiloSandboxAllocation, + getSandboxAllocation, getSandboxAllocationRequest, sandboxAllocationSchema, } from '@kilocode/worker-utils/sandbox-allocation'; @@ -12,7 +12,7 @@ import { import { classifySandboxId, selectSandboxForNewSession } from './sandbox-id.js'; import type { Env } from './types.js'; -const configured = { +const configured = Object.assign({} as Env, { SANDBOX_SELECTION_IDS: 'org-id', VERCEL_TOKEN: 'test-token', VERCEL_TEAM_ID: 'team-id', @@ -22,7 +22,7 @@ const configured = { VERCEL_SANDBOX_RUNTIME: 'node24', VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: '300000', VERCEL_SANDBOX_EXTEND_DURATION_MS: '600000', -} satisfies Partial; +}); const owner = { userId: 'oauth/user', orgId: 'org-id' }; type Capabilities = ReturnType; @@ -50,14 +50,14 @@ describe('sandbox selection policy', () => { { name: 'Vercel with provider-default resources', overrides: { VERCEL_SANDBOX_ORG_IDS: owner.orgId }, - expected: { provider: { id: 'vercel', account: 'kilo' }, instanceType: 'default' }, + expected: getSandboxAllocationRequest('cloudflare-containers-standard-3'), }, { name: 'Vercel with personal Vercel enrollment', overrides: { VERCEL_SANDBOX_ORG_IDS: '*', }, - expected: { provider: { id: 'vercel', account: 'kilo' }, instanceType: 'default' }, + expected: getSandboxAllocationRequest('cloudflare-containers-standard-3'), }, { name: 'isolated Cloudflare outside the former allowlist', @@ -114,13 +114,11 @@ describe('sandbox selection policy', () => { }); expect(actual.provider).toBe(expected.provider.id); if (actual.provider === 'cloudflare') { - expect(classifySandboxId(actual.sandboxId)).toBe( - expected.instanceType === 'shared' ? 'shared' : 'isolated-small' - ); + expect(classifySandboxId(actual.sandboxId)).toBe('isolated-small'); } }); - it('does not authorize a Kilo allocation from a BYOC capability with the same size', () => { + it('authorizes a BYOC allocation from the matching BYOC capability', () => { expect( isSandboxAllocationAvailable( { @@ -133,7 +131,7 @@ describe('sandbox selection policy', () => { }, 'vercel-small' ) - ).toBe(false); + ).toBe(true); }); it.each([{ userId: owner.userId }, owner])( @@ -183,12 +181,10 @@ describe('sandbox selection policy', () => { expect(availableOptions(capabilities)).toEqual([ getSandboxAllocationRequest('cloudflare-containers-standard-3'), getSandboxAllocationRequest('cloudflare-shared'), - getSandboxAllocationRequest('vercel-small'), - getSandboxAllocationRequest('vercel-large'), ]); expect( capabilities.options.some( - option => getKiloSandboxAllocation(option.allocation) === 'isolated-standard' + option => getSandboxAllocation(option.allocation) === 'isolated-standard' ) ).toBe(false); }); @@ -198,8 +194,6 @@ describe('sandbox selection policy', () => { expect(availableOptions(getSandboxSelectionCapabilities(missingContainers, owner))).toEqual([ getSandboxAllocationRequest('cloudflare-containers-standard-3'), getSandboxAllocationRequest('cloudflare-shared'), - getSandboxAllocationRequest('vercel-small'), - getSandboxAllocationRequest('vercel-large'), ]); const enrolledContainers = { @@ -210,8 +204,6 @@ describe('sandbox selection policy', () => { getSandboxAllocationRequest('cloudflare-shared'), getSandboxAllocationRequest('cloudflare-containers-standard-3'), getSandboxAllocationRequest('cloudflare-containers-standard-4'), - getSandboxAllocationRequest('vercel-small'), - getSandboxAllocationRequest('vercel-large'), ]); }); @@ -219,7 +211,7 @@ describe('sandbox selection policy', () => { const env = { ...configured, SANDBOX_SELECTION_IDS: owner.userId } as Env; const capabilities = getSandboxSelectionCapabilities(env, { userId: owner.userId }); expect(capabilities.enabled).toBe(true); - expect(availableOptions(capabilities)).toHaveLength(4); + expect(availableOptions(capabilities)).toHaveLength(2); }); it('enables personal selection for wildcard rollouts', () => { @@ -246,11 +238,15 @@ describe('sandbox selection policy', () => { } as Env; const capabilities = getSandboxSelectionCapabilities(env, owner); expect(capabilities.enabled).toBe(true); - expect(availableOptions(capabilities)).toHaveLength(5); + expect(availableOptions(capabilities)).toHaveLength(3); expect(capabilities.options.every(option => !('available' in option))).toBe(true); expect(JSON.stringify(capabilities)).not.toContain('test-token'); for (const preset of sandboxAllocationSchema.options) { - expect(() => assertSandboxAllocationAvailable(env, owner, preset)).not.toThrow(); + if (preset.startsWith('vercel-')) + expect(() => assertSandboxAllocationAvailable(env, owner, preset)).toThrow( + 'Configure Vercel BYOC' + ); + else expect(() => assertSandboxAllocationAvailable(env, owner, preset)).not.toThrow(); } }); @@ -266,13 +262,48 @@ describe('sandbox selection policy', () => { ]); for (const preset of ['vercel-small', 'vercel-large'] as const) { expect(() => assertSandboxAllocationAvailable(env, owner, preset)).toThrow( - 'not configured' + 'Configure Vercel BYOC' ); } expect(() => assertSandboxAllocationAvailable(env, owner, 'isolated-standard')).not.toThrow(); } ); + it('makes only Vercel allocations available to BYOC-enrolled organizations', () => { + const env = { ...configured, BYOC_VERCEL_IDS: owner.orgId } as Env; + expect(getSandboxSelectionCapabilities(env, owner).defaultDestination).toEqual({ + provider: { id: 'vercel', account: 'byoc' }, + instanceType: 'small', + }); + expect( + getSandboxSelectionCapabilities(env, owner) + .options.filter(option => !option.unavailableReason) + .map(option => option.allocation) + ).toEqual([ + getSandboxAllocationRequest('vercel-small'), + getSandboxAllocationRequest('vercel-large'), + ]); + for (const preset of ['cloudflare-single', 'cloudflare-shared'] as const) { + expect(() => assertSandboxAllocationAvailable(env, owner, preset)).toThrow( + 'cannot select non-Vercel allocations' + ); + } + expect(() => assertSandboxAllocationAvailable(env, owner, 'vercel-small')).not.toThrow(); + }); + + it('offers only Vercel allocations for enrolled personal accounts without platform credentials', () => { + const env = { SANDBOX_SELECTION_IDS: '*', BYOC_VERCEL_IDS: '*' } as Env; + const capabilities = getSandboxSelectionCapabilities(env, { userId: owner.userId }); + expect(capabilities.defaultDestination).toEqual({ + provider: { id: 'vercel', account: 'byoc' }, + instanceType: 'small', + }); + expect(availableOptions(capabilities)).toEqual([ + getSandboxAllocationRequest('vercel-small'), + getSandboxAllocationRequest('vercel-large'), + ]); + }); + it('fails Vercel closed for enforced organization billing', () => { const env = { ...configured, @@ -286,7 +317,7 @@ describe('sandbox selection policy', () => { ]); for (const preset of ['vercel-small', 'vercel-large'] as const) { expect(() => assertSandboxAllocationAvailable(env, owner, preset)).toThrow( - 'enforced compute billing' + 'Configure Vercel BYOC' ); } expect(() => assertSandboxAllocationAvailable(env, owner, 'cloudflare-single')).not.toThrow(); @@ -345,7 +376,7 @@ describe('sandbox selection policy', () => { getSandboxAllocationRequest('cloudflare-containers-standard-4') ); const containers = orgOnlyCapabilities.options.find( - option => getKiloSandboxAllocation(option.allocation) === 'cloudflare-containers-standard-4' + option => getSandboxAllocation(option.allocation) === 'cloudflare-containers-standard-4' ); expect(containers?.unavailableReason).toBeTruthy(); }); @@ -372,19 +403,27 @@ describe('sandbox selection policy', () => { }); it('reports a repository-unavailable reason while keeping a provider-available option listed', () => { - const gitlab = getSandboxSelectionCapabilities(configured as Env, owner, { - repositoryType: 'gitlab', - }); + const gitlab = getSandboxSelectionCapabilities( + { ...configured, BYOC_VERCEL_IDS: owner.orgId } as Env, + owner, + { + repositoryType: 'gitlab', + } + ); const vercel = gitlab.options.find( - option => getKiloSandboxAllocation(option.allocation) === 'vercel-small' + option => getSandboxAllocation(option.allocation) === 'vercel-small' ); expect(vercel?.unavailableReason).toBeUndefined(); expect(vercel?.repositoryUnavailableReason).toBeTruthy(); expect(isSandboxAllocationAvailable(gitlab, 'vercel-small')).toBe(false); - const github = getSandboxSelectionCapabilities(configured as Env, owner, { - repositoryType: 'github', - }); + const github = getSandboxSelectionCapabilities( + { ...configured, BYOC_VERCEL_IDS: owner.orgId } as Env, + owner, + { + repositoryType: 'github', + } + ); expect(isSandboxAllocationAvailable(github, 'vercel-small')).toBe(true); }); @@ -394,7 +433,7 @@ describe('sandbox selection policy', () => { capability, }); const vercel = capabilities.options.find( - option => getKiloSandboxAllocation(option.allocation) === 'vercel-small' + option => getSandboxAllocation(option.allocation) === 'vercel-small' ); expect(vercel?.unavailableReason).toBeTruthy(); expect(vercel?.repositoryUnavailableReason).toBeUndefined(); diff --git a/services/cloud-agent-next/src/sandbox-selection.ts b/services/cloud-agent-next/src/sandbox-selection.ts index 52f23387c20..cb92dbfc9f2 100644 --- a/services/cloud-agent-next/src/sandbox-selection.ts +++ b/services/cloud-agent-next/src/sandbox-selection.ts @@ -1,17 +1,16 @@ +import { enrolledByocVercelOwner } from './byoc/vercel-owner.js'; import { TRPCError } from '@trpc/server'; import { CLOUDFLARE_SANDBOX_SMALL_NATIVE_ALLOCATION, SELECTABLE_SANDBOX_ALLOCATIONS, - getKiloSandboxAllocation, + getSandboxAllocation, getSandboxAllocationProvider, getSandboxAllocationRequest, isSandboxAllocationAvailable, type SandboxAllocation, type SandboxSelectionCapabilities, } from '@kilocode/worker-utils/sandbox-allocation'; -import { parseVercelSandboxRuntimeConfig } from './agent-sandbox/vercel/vercel-runtime-config.js'; import { isCloudflareContainersEnrolled } from './agent-sandbox/cloudflare-containers/cloudflare-containers-runtime-config.js'; -import { isCloudAgentContainerBillingEnabled } from './container-billing-rollout.js'; import { credentialContainmentEnabled, getDefaultSandboxDestination, @@ -45,6 +44,17 @@ export function sandboxAllocationProviderCapabilityReason( : 'Vercel sandboxes do not include the security-remediation command-line tools'; } +export function isByocIncompatibleAllocation( + env: Env, + owner: { userId?: string; orgId?: string }, + allocation: SandboxAllocation +): boolean { + return ( + enrolledByocVercelOwner(env, owner) !== undefined && + getSandboxAllocationProvider(allocation) !== 'vercel' + ); +} + export function sandboxAllocationUnavailableReason( env: Env, owner: SelectionOwner, @@ -53,16 +63,15 @@ export function sandboxAllocationUnavailableReason( ): string | undefined { const capabilityReason = sandboxAllocationProviderCapabilityReason(allocation, capability); if (capabilityReason) return capabilityReason; + if (isByocIncompatibleAllocation(env, owner, allocation)) + return 'BYOC Vercel accounts cannot select non-Vercel allocations'; const provider = getSandboxAllocationProvider(allocation); switch (provider) { case 'cloudflare': return undefined; case 'vercel': - if (!parseVercelSandboxRuntimeConfig(env)) return 'Vercel sandboxes are not configured'; - if (isCloudAgentContainerBillingEnabled(env, owner)) { - return 'Vercel sandboxes do not support enforced compute billing'; - } - return undefined; + if (enrolledByocVercelOwner(env, owner) !== undefined) return undefined; + return 'Configure Vercel BYOC in Cloud Agent settings to use Vercel compute'; case 'cloudflare-containers': if (!isCloudflareContainersEnrolled(env, { orgId: owner.orgId })) { return 'Cloudflare containers are not enabled for this account'; @@ -100,9 +109,9 @@ type SelectionOption = SandboxSelectionCapabilities['options'][number]; */ function nativeSmallInPlaceOfSingle(options: SelectionOption[]): SelectionOption[] { const isNative = (option: SelectionOption) => - getKiloSandboxAllocation(option.allocation) === CLOUDFLARE_SANDBOX_SMALL_NATIVE_ALLOCATION; + getSandboxAllocation(option.allocation) === CLOUDFLARE_SANDBOX_SMALL_NATIVE_ALLOCATION; const single = options.find( - option => getKiloSandboxAllocation(option.allocation) === 'cloudflare-single' + option => getSandboxAllocation(option.allocation) === 'cloudflare-single' ); const nativeAvailable = options.some( option => isNative(option) && option.unavailableReason === undefined @@ -187,15 +196,19 @@ export function assertSandboxAllocationAvailable( message: 'Sandbox selection is not enabled for this owner', }); } - if (!isSandboxAllocationAvailable(capabilities, allocation)) { + const unavailableReason = isByocIncompatibleAllocation(env, owner, allocation) + ? sandboxAllocationUnavailableReason(env, owner, allocation, capability) + : undefined; + if (unavailableReason !== undefined || !isSandboxAllocationAvailable(capabilities, allocation)) { const option = capabilities.options.find( - candidate => getKiloSandboxAllocation(candidate.allocation) === allocation + candidate => getSandboxAllocation(candidate.allocation) === allocation ); throw new TRPCError({ code: 'BAD_REQUEST', message: option?.unavailableReason ?? option?.repositoryUnavailableReason ?? + unavailableReason ?? sandboxAllocationUnavailableReason(env, owner, allocation, capability) ?? 'Sandbox allocation is unavailable', }); diff --git a/services/cloud-agent-next/src/server-stream-ticket.test.ts b/services/cloud-agent-next/src/server-stream-ticket.test.ts index d2464fda3de..1ba9c6d5ce1 100644 --- a/services/cloud-agent-next/src/server-stream-ticket.test.ts +++ b/services/cloud-agent-next/src/server-stream-ticket.test.ts @@ -42,6 +42,14 @@ vi.mock('cloudflare:workers', () => ({ }, })); +vi.mock('./byoc/vercel-runtime-identity.js', () => ({ + getVercelRuntimeIdentity: vi.fn().mockResolvedValue({ + wrapperVersion: 'test-wrapper', + releasedAt: '2026-10-24', + digest: 'test-runtime-digest', + }), +})); + vi.mock('./router.js', () => ({ appRouter: {}, })); diff --git a/services/cloud-agent-next/src/server.test.ts b/services/cloud-agent-next/src/server.test.ts index ed19c887fae..c7a4c8f803d 100644 --- a/services/cloud-agent-next/src/server.test.ts +++ b/services/cloud-agent-next/src/server.test.ts @@ -76,6 +76,14 @@ vi.mock('cloudflare:workers', () => ({ }, })); +vi.mock('./byoc/vercel-runtime-identity.js', () => ({ + getVercelRuntimeIdentity: vi.fn().mockResolvedValue({ + wrapperVersion: 'test-wrapper', + releasedAt: '2026-10-24', + digest: 'test-runtime-digest', + }), +})); + vi.mock('./router.js', () => ({ appRouter: {}, })); @@ -149,6 +157,8 @@ const { const secret = 'test-secret'; type MockEnv = { + BYOC_VERCEL_IDS?: string; + VERCEL_SNAPSHOT_BUILD: { getByName: ReturnType }; NEXTAUTH_SECRET: string; Sandbox: unknown; SandboxSmall: unknown; @@ -175,6 +185,7 @@ type MockEnv = { function createEnv(): MockEnv { return { NEXTAUTH_SECRET: secret, + VERCEL_SNAPSHOT_BUILD: { getByName: vi.fn() }, Sandbox: {}, SandboxSmall: {}, HYPERDRIVE: { connectionString: 'postgres://test' }, @@ -2461,3 +2472,318 @@ describe('server preview host dispatch', () => { expect(previewHost.headers.get('cache-control')).toBeNull(); }); }); + +describe('server /internal/byoc/vercel-enrollment/:organizationId', () => { + const organizationId = '11111111-1111-4111-8111-111111111111'; + + it('rejects enrollment requests without the internal API key', async () => { + const env = createEnv(); + + const response = await fetchWorker( + new Request(`http://worker.test/internal/byoc/vercel-enrollment/${organizationId}`), + env + ); + + expect(response.status).toBe(401); + }); + + it('rejects invalid organization IDs', async () => { + const env = createEnv(); + + const response = await fetchWorker( + new Request('http://worker.test/internal/byoc/vercel-enrollment/not-an-organization-id', { + headers: { 'x-internal-api-key': 'test-internal-secret' }, + }), + env + ); + + expect(response.status).toBe(400); + }); + + it.each([ + { enrolledOrganizations: undefined, enrolled: false }, + { enrolledOrganizations: '22222222-2222-4222-8222-222222222222', enrolled: false }, + { enrolledOrganizations: organizationId, enrolled: true }, + { enrolledOrganizations: '*', enrolled: true }, + ])('returns Worker-configured enrollment for $enrolledOrganizations', async value => { + const env = createEnv(); + env.BYOC_VERCEL_IDS = value.enrolledOrganizations; + + const response = await fetchWorker( + new Request(`http://worker.test/internal/byoc/vercel-enrollment/${organizationId}`, { + headers: { 'x-internal-api-key': 'test-internal-secret' }, + }), + env + ); + + expect(response.status).toBe(200); + expect(response.headers.get('Cache-Control')).toBe('no-store'); + await expect(response.json()).resolves.toEqual({ enrolled: value.enrolled }); + }); +}); + +describe('server /internal/byoc/vercel-snapshot-build/start', () => { + const buildInput = { + organizationId: '11111111-1111-4111-8111-111111111111', + credentialId: '22222222-2222-4222-8222-222222222222', + buildGeneration: '33333333-3333-4333-8333-333333333333', + }; + + function buildRequest(): Request { + return new Request('http://worker.test/internal/byoc/vercel-snapshot-build/start', { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-internal-api-key': 'test-internal-secret', + }, + body: JSON.stringify(buildInput), + }); + } + + it('rejects snapshot builds for organizations outside the Worker enrollment list', async () => { + const env = createEnv(); + + const response = await fetchWorker(buildRequest(), env); + + expect(response.status).toBe(403); + expect(env.VERCEL_SNAPSHOT_BUILD.getByName).not.toHaveBeenCalled(); + }); + + it('starts snapshot builds for enrolled organizations', async () => { + const env = createEnv(); + env.BYOC_VERCEL_IDS = buildInput.organizationId; + const start = vi.fn().mockResolvedValue(undefined); + env.VERCEL_SNAPSHOT_BUILD.getByName.mockReturnValue({ start }); + + const response = await fetchWorker(buildRequest(), env); + + expect(response.status).toBe(202); + expect(start).toHaveBeenCalledWith(buildInput); + }); + + it('starts snapshot builds for an enrolled personal owner without inferring UUID ownership', async () => { + const env = createEnv(); + const input = { + userId: 'oauth/vercel-user', + credentialId: buildInput.credentialId, + buildGeneration: buildInput.buildGeneration, + }; + env.BYOC_VERCEL_IDS = input.userId; + const start = vi.fn().mockResolvedValue(undefined); + env.VERCEL_SNAPSHOT_BUILD.getByName.mockReturnValue({ start }); + + const response = await fetchWorker( + new Request('http://worker.test/internal/byoc/vercel-snapshot-build/start', { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-internal-api-key': 'test-internal-secret', + }, + body: JSON.stringify(input), + }), + env + ); + + expect(response.status).toBe(202); + expect(env.VERCEL_SNAPSHOT_BUILD.getByName).toHaveBeenCalledWith('user:oauth/vercel-user'); + expect(start).toHaveBeenCalledWith(input); + }); + + it('rejects snapshot starts that provide both owner identities', async () => { + const env = createEnv(); + const input = { + ...buildInput, + userId: 'oauth/vercel-user', + }; + + const response = await fetchWorker( + new Request('http://worker.test/internal/byoc/vercel-snapshot-build/start', { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-internal-api-key': 'test-internal-secret', + }, + body: JSON.stringify(input), + }), + env + ); + + expect(response.status).toBe(400); + expect(env.VERCEL_SNAPSHOT_BUILD.getByName).not.toHaveBeenCalled(); + }); +}); + +describe('server /internal/byoc/vercel-snapshot-build/cleanup', () => { + const cleanupInput = { + organizationId: '11111111-1111-4111-8111-111111111111', + credentialId: '22222222-2222-4222-8222-222222222222', + buildGeneration: '33333333-3333-4333-8333-333333333333', + snapshotId: 'snap_persisted', + }; + + it('rejects cleanup requests without the internal API key', async () => { + const env = createEnv(); + + const response = await fetchWorker( + new Request('http://worker.test/internal/byoc/vercel-snapshot-build/cleanup', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(cleanupInput), + }), + env + ); + + expect(response.status).toBe(401); + expect(env.VERCEL_SNAPSHOT_BUILD.getByName).not.toHaveBeenCalled(); + }); + + it('rejects invalid cleanup identity before calling the Durable Object', async () => { + const env = createEnv(); + + const response = await fetchWorker( + new Request('http://worker.test/internal/byoc/vercel-snapshot-build/cleanup', { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-internal-api-key': 'test-internal-secret', + }, + body: JSON.stringify({ ...cleanupInput, credentialId: 'invalid' }), + }), + env + ); + + expect(response.status).toBe(400); + expect(env.VERCEL_SNAPSHOT_BUILD.getByName).not.toHaveBeenCalled(); + }); + + it('passes the persisted runtime snapshot to the organization Durable Object', async () => { + const env = createEnv(); + const cleanup = vi.fn().mockResolvedValue(undefined); + env.VERCEL_SNAPSHOT_BUILD.getByName.mockReturnValue({ cleanup }); + + const response = await fetchWorker( + new Request('http://worker.test/internal/byoc/vercel-snapshot-build/cleanup', { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-internal-api-key': 'test-internal-secret', + }, + body: JSON.stringify(cleanupInput), + }), + env + ); + + expect(response.status).toBe(204); + expect(env.VERCEL_SNAPSHOT_BUILD.getByName).toHaveBeenCalledWith(cleanupInput.organizationId); + expect(cleanup).toHaveBeenCalledWith(cleanupInput); + }); + + it('passes personal cleanup to the user-keyed Durable Object', async () => { + const env = createEnv(); + const input = { + userId: 'oauth/vercel-user', + credentialId: cleanupInput.credentialId, + buildGeneration: cleanupInput.buildGeneration, + snapshotId: cleanupInput.snapshotId, + }; + const cleanup = vi.fn().mockResolvedValue(undefined); + env.VERCEL_SNAPSHOT_BUILD.getByName.mockReturnValue({ cleanup }); + + const response = await fetchWorker( + new Request('http://worker.test/internal/byoc/vercel-snapshot-build/cleanup', { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-internal-api-key': 'test-internal-secret', + }, + body: JSON.stringify(input), + }), + env + ); + + expect(response.status).toBe(204); + expect(env.VERCEL_SNAPSHOT_BUILD.getByName).toHaveBeenCalledWith('user:oauth/vercel-user'); + expect(cleanup).toHaveBeenCalledWith(input); + }); + + it('rejects snapshot cleanup that provides both owner identities', async () => { + const env = createEnv(); + const input = { + ...cleanupInput, + userId: 'oauth/vercel-user', + }; + + const response = await fetchWorker( + new Request('http://worker.test/internal/byoc/vercel-snapshot-build/cleanup', { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-internal-api-key': 'test-internal-secret', + }, + body: JSON.stringify(input), + }), + env + ); + + expect(response.status).toBe(400); + expect(env.VERCEL_SNAPSHOT_BUILD.getByName).not.toHaveBeenCalled(); + }); + + it('returns a safe error when cleanup cannot be completed', async () => { + const env = createEnv(); + env.VERCEL_SNAPSHOT_BUILD.getByName.mockReturnValue({ + cleanup: vi.fn().mockRejectedValue(new Error('provider request details')), + }); + + const response = await fetchWorker( + new Request('http://worker.test/internal/byoc/vercel-snapshot-build/cleanup', { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-internal-api-key': 'test-internal-secret', + }, + body: JSON.stringify(cleanupInput), + }), + env + ); + + expect(response.status).toBe(502); + await expect(response.text()).resolves.toBe('Snapshot build resources could not be cleaned up'); + }); +}); + +describe('server /internal/byoc/vercel-enrollment/user/:userId', () => { + const userId = 'oauth/vercel-user'; + + it('returns Worker-configured enrollment for an encoded personal user id', async () => { + const env = createEnv(); + env.BYOC_VERCEL_IDS = userId; + + const response = await fetchWorker( + new Request( + `http://worker.test/internal/byoc/vercel-enrollment/user/${encodeURIComponent(userId)}`, + { headers: { 'x-internal-api-key': 'test-internal-secret' } } + ), + env + ); + + expect(response.status).toBe(200); + expect(response.headers.get('Cache-Control')).toBe('no-store'); + await expect(response.json()).resolves.toEqual({ enrolled: true }); + }); + + it('keeps an empty personal enrollment list disabled', async () => { + const env = createEnv(); + + const response = await fetchWorker( + new Request( + `http://worker.test/internal/byoc/vercel-enrollment/user/${encodeURIComponent(userId)}`, + { headers: { 'x-internal-api-key': 'test-internal-secret' } } + ), + env + ); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual({ enrolled: false }); + }); +}); diff --git a/services/cloud-agent-next/src/server.ts b/services/cloud-agent-next/src/server.ts index 243fc5c8016..62bca06c772 100644 --- a/services/cloud-agent-next/src/server.ts +++ b/services/cloud-agent-next/src/server.ts @@ -1,3 +1,4 @@ +import { byocVercelOwnerSchema } from './byoc/vercel-owner.js'; import { Hono } from 'hono'; import type { Context, Next } from 'hono'; import { trpcServer } from '@hono/trpc-server'; @@ -78,9 +79,36 @@ import { widgetApprovalKeyRequestSchema, type WidgetApproval, } from './widget-approval-key.js'; +import { getVercelSnapshotBuildStub } from './byoc/vercel-snapshot-build-stub.js'; +import { getVercelRuntimeIdentity } from './byoc/vercel-runtime-identity.js'; +import { z } from 'zod'; +import { isOrgInList } from './sandbox-id.js'; const app = new Hono(); +const SnapshotBuildOwnerSchema = byocVercelOwnerSchema; +const SnapshotBuildStartInputSchema = z + .object({ + credentialId: z.uuid(), + buildGeneration: z.uuid(), + }) + .and(SnapshotBuildOwnerSchema); +const SnapshotBuildCleanupInputSchema = z + .object({ + credentialId: z.uuid(), + buildGeneration: z.uuid(), + snapshotId: z.string().min(1).max(256).optional(), + }) + .and(SnapshotBuildOwnerSchema); + +function snapshotBuildOwnerKey(input: z.infer): string { + return input.organizationId !== undefined ? input.organizationId : `user:${input.userId}`; +} + +function snapshotBuildOwnerId(input: z.infer): string { + return input.organizationId !== undefined ? input.organizationId : input.userId; +} + function isAllowedWebSocketOrigin(env: Env, origin: string | undefined): boolean { const allowedOrigins = (env.WS_ALLOWED_ORIGINS || '') .split(',') @@ -283,6 +311,83 @@ registerControlLogRoutes(app); registerRuntimeArtifactRoutes(app); registerStartupFailureRoute(app); +app.get('/internal/byoc/vercel-enrollment/:organizationId', (c: Context) => { + const unauthorized = requireInternalApi(c); + if (unauthorized) return unauthorized; + + const organizationId = z.uuid().safeParse(c.req.param('organizationId')); + if (!organizationId.success) return c.text('Invalid organization ID', 400); + + return c.json({ enrolled: isOrgInList(c.env.BYOC_VERCEL_IDS, organizationId.data) }, 200, { + 'Cache-Control': 'no-store', + }); +}); + +app.get('/internal/byoc/vercel-enrollment/user/:userId', (c: Context) => { + const unauthorized = requireInternalApi(c); + if (unauthorized) return unauthorized; + + const userId = z.string().min(1).safeParse(c.req.param('userId')); + if (!userId.success) return c.text('Invalid user ID', 400); + + return c.json({ enrolled: isOrgInList(c.env.BYOC_VERCEL_IDS, userId.data) }, 200, { + 'Cache-Control': 'no-store', + }); +}); + +app.get('/internal/byoc/vercel-runtime-identity', async (c: Context) => { + const unauthorized = requireInternalApi(c); + if (unauthorized) return unauthorized; + + try { + return c.json(await getVercelRuntimeIdentity(c.env), 200, { 'Cache-Control': 'no-store' }); + } catch { + return c.text('Vercel runtime identity could not be resolved', 502); + } +}); + +app.post('/internal/byoc/vercel-snapshot-build/start', async (c: Context) => { + const unauthorized = requireInternalApi(c); + if (unauthorized) return unauthorized; + + const input = SnapshotBuildStartInputSchema.safeParse(await c.req.json().catch(() => null)); + if (!input.success) return c.text('Invalid request', 400); + if (!isOrgInList(c.env.BYOC_VERCEL_IDS, snapshotBuildOwnerId(input.data))) { + return c.text('Owner is not enrolled in Vercel compute', 403); + } + + try { + await withDORetry( + () => getVercelSnapshotBuildStub(c.env, snapshotBuildOwnerKey(input.data)), + stub => stub.start(input.data), + 'startVercelSnapshotBuild' + ); + } catch { + return c.text('Snapshot build could not be started', 502); + } + return c.json({ accepted: true }, 202); +}); + +app.post('/internal/byoc/vercel-snapshot-build/cleanup', async (c: Context) => { + const unauthorized = requireInternalApi(c); + if (unauthorized) return unauthorized; + + const input = SnapshotBuildCleanupInputSchema.safeParse(await c.req.json().catch(() => null)); + if (!input.success) return c.text('Invalid request', 400); + + try { + await withDORetry( + () => getVercelSnapshotBuildStub(c.env, snapshotBuildOwnerKey(input.data)), + stub => stub.cleanup(input.data), + 'cleanupVercelSnapshotBuild' + ); + } catch { + return c.text('Snapshot build resources could not be cleaned up', 502); + } + + return c.body(null, 204); +}); + app.get('/sandbox-control/:sandboxId', async (c: Context) => { return admitSandboxWrapperUpgrade(c.req.raw, c.env, c.req.param('sandboxId') ?? ''); }); diff --git a/services/cloud-agent-next/src/session-prepare.test.ts b/services/cloud-agent-next/src/session-prepare.test.ts index fe893bb0818..b57813ba48e 100644 --- a/services/cloud-agent-next/src/session-prepare.test.ts +++ b/services/cloud-agent-next/src/session-prepare.test.ts @@ -743,8 +743,9 @@ describe('prepareSession endpoint', () => { it('registers full lazy-prep metadata in one DO call', async () => { selectControlPlaneSandboxMock.mockResolvedValueOnce({ - kind: 'isolated', - sandboxId: 'crv-abcdef', + sandboxKind: 'code-review', + provider: 'cloudflare', + sandboxId: 'sbx-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', }); const doStub = createMockDOStub(); const caller = appRouter.createCaller(createInternalApiContext({ doStub })); @@ -826,7 +827,7 @@ describe('prepareSession endpoint', () => { finalization: { autoCommit: true, condenseOnComplete: true, gateThreshold: 'warning' }, callback: { target: { url: 'https://example.com/callback' } }, workspace: expect.objectContaining({ - sandboxId: 'crv-abcdef', + sandboxId: 'sbx-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', sandboxProvider: 'cloudflare', shallow: true, credentialContainment: { @@ -2149,7 +2150,7 @@ describe('start endpoint', () => { ).rejects.toThrow('metadata invalid'); }); - it('forwards Vercel enrollment configuration and persists only the selected identity', async () => { + it('ignores retired platform Vercel enrollment configuration', async () => { const doStub = createMockDOStub(); const context = createInternalApiContext({ doStub }); Object.assign(context.env, { @@ -2190,7 +2191,7 @@ describe('start endpoint', () => { metadata: expect.objectContaining({ workspace: expect.objectContaining({ sandboxId: expect.stringMatching(/^sbx-/), - sandboxProvider: 'vercel', + sandboxProvider: 'cloudflare-containers', }), }), }) diff --git a/services/cloud-agent-next/src/session-profile.ts b/services/cloud-agent-next/src/session-profile.ts index 2aa50e96cff..1c95e3e191e 100644 --- a/services/cloud-agent-next/src/session-profile.ts +++ b/services/cloud-agent-next/src/session-profile.ts @@ -1,6 +1,7 @@ import type { SessionProfileBundle } from './persistence/schemas.js'; import type { CloudAgentSessionState } from './persistence/types.js'; import type { SessionMetadata } from './persistence/session-metadata.js'; +import { BUILTIN_AGENT_MODES } from './schema.js'; export type { SessionProfileBundle } from './persistence/schemas.js'; @@ -29,3 +30,20 @@ export function readProfileBundle( export function profileFromMetadata(metadata: CloudAgentSessionState): SessionProfileBundle { return readProfileBundle(metadata); } + +/** + * A mode is valid when it is built in or matches a runtime agent installed on + * the session. Both the legacy and the control-plane admission paths use this + * single check. + */ +export function validateModeAgainstRuntimeAgents( + metadata: Pick, + mode = metadata.agent?.mode +): string | null { + if (!mode || BUILTIN_AGENT_MODES.has(mode)) return null; + + const knownSlugs = new Set((readProfileBundle(metadata).runtimeAgents ?? []).map(a => a.slug)); + if (knownSlugs.has(mode)) return null; + + return `Mode "${mode}" is not a built-in and does not match any runtimeAgents on this session`; +} diff --git a/services/cloud-agent-next/src/session/control-plane-session-input.test.ts b/services/cloud-agent-next/src/session/control-plane-session-input.test.ts index 7852fb732ab..f7d22fc33d0 100644 --- a/services/cloud-agent-next/src/session/control-plane-session-input.test.ts +++ b/services/cloud-agent-next/src/session/control-plane-session-input.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; import type { Env } from '../types.js'; -import { vercelBillingIdentity } from '../container-usage-context.js'; import { buildControlPlaneCreateInput, buildControlPlaneRegisterInput, @@ -45,6 +44,44 @@ describe('buildControlPlanePromptPayload', () => { }); describe('buildControlPlaneCreateInput', () => { + it.each([ + { organizationId: '11111111-1111-4111-8111-111111111111' }, + { userId: 'oauth/google/personal-owner' }, + ])('pins the customer credential without Kilo compute billing for %j', async owner => { + const source = { + kind: 'byoc' as const, + ...owner, + credentialId: '22222222-2222-4222-8222-222222222222', + }; + const input = await buildControlPlaneCreateInput({ + command: { + identity: { + sessionId: 'workspace_12345678-1234-1234-1234-123456789abc', + userId: 'usr_1', + orgId: source.organizationId, + }, + auth: { kiloSessionId: 'ses_12345678901234567890123456', kilocodeToken: MODERN_KILO_TOKEN }, + message: { + initialMessageId: 'msg_018f1e2d3c4bAbCdEfGhIjKlMn', + turn: { type: 'prompt', prompt: 'hi' }, + }, + agent: { mode: 'code', model: FULL_MODEL }, + workspace: { + sandboxId: 'ses-0123456789abcdef', + sandboxProvider: 'vercel', + sandboxProviderBinding: { kind: 'vercel', source }, + }, + }, + env: { CLOUD_AGENT_CONTAINER_BILLING_ENABLED: 'true' } as Env, + attachments: [], + agent: { mode: 'code', model: FULL_MODEL }, + }); + expect(input.sandboxSelection).toEqual({ + provider: 'vercel', + configuration: { provider: 'vercel', resources: { vcpus: 2, memory: 4096 } }, + byoc: { ...owner, credentialId: source.credentialId }, + }); + }); it.each([ { egressMode: undefined, egressAllowlist: undefined }, { egressMode: 'unrestricted' as const, egressAllowlist: undefined }, @@ -99,35 +136,35 @@ describe('buildControlPlaneCreateInput', () => { [undefined, { vcpus: 2, memory: 4096 }, 'SandboxVercelSmall'], ['vercel-large', { vcpus: 4, memory: 8192 }, 'SandboxVercelLarge'], ] as const)( - 'pins %s Vercel selection to a billable size', - async (allocation, resources, className) => { - const input = await buildControlPlaneCreateInput({ - command: { - identity: { - sessionId: 'workspace_12345678-1234-1234-1234-123456789abc', - userId: 'usr_1', - }, - auth: { - kiloSessionId: 'ses_12345678901234567890123456', - kilocodeToken: MODERN_KILO_TOKEN, - }, - message: { - initialMessageId: 'msg_018f1e2d3c4bAbCdEfGhIjKlMn', - turn: { type: 'prompt', prompt: 'hi' }, + 'rejects retired platform Vercel selection %s', + async (allocation, _resources, _className) => { + await expect( + buildControlPlaneCreateInput({ + command: { + identity: { + sessionId: 'workspace_12345678-1234-1234-1234-123456789abc', + userId: 'usr_1', + }, + auth: { + kiloSessionId: 'ses_12345678901234567890123456', + kilocodeToken: MODERN_KILO_TOKEN, + }, + message: { + initialMessageId: 'msg_018f1e2d3c4bAbCdEfGhIjKlMn', + turn: { type: 'prompt', prompt: 'hi' }, + }, + agent: { mode: 'code', model: FULL_MODEL }, + workspace: { + sandboxId: 'ses-0123456789abcdef', + sandboxProvider: 'vercel', + ...(allocation === undefined ? {} : { sandboxAllocation: allocation }), + }, }, + env: {} as Env, + attachments: [], agent: { mode: 'code', model: FULL_MODEL }, - workspace: { - sandboxId: 'ses-0123456789abcdef', - sandboxProvider: 'vercel', - ...(allocation === undefined ? {} : { sandboxAllocation: allocation }), - }, - }, - env: {} as Env, - attachments: [], - agent: { mode: 'code', model: FULL_MODEL }, - }); - expect(input.sandboxSelection.configuration).toEqual({ provider: 'vercel', resources }); - expect(vercelBillingIdentity(resources).className).toBe(className); + }) + ).rejects.toThrow('Kilo-paid Vercel compute is retired'); } ); @@ -217,7 +254,7 @@ describe('buildControlPlaneCreateInput', () => { sessionId: 'workspace_12345678-1234-1234-1234-123456789abc', userId: 'usr_1', }, - auth: { kiloSessionId: 'ses_12345678901234567890123456', kilocodeToken: 'token' }, + auth: { kiloSessionId: 'ses_12345678901234567890123456', kilocodeToken: MODERN_KILO_TOKEN }, message: { initialMessageId: 'msg_018f1e2d3c4bAbCdEfGhIjKlMn', turn: { type: 'prompt', prompt: 'hi' }, diff --git a/services/cloud-agent-next/src/session/control-plane-session-input.ts b/services/cloud-agent-next/src/session/control-plane-session-input.ts index 116d1c8d1c5..d60d24e0e19 100644 --- a/services/cloud-agent-next/src/session/control-plane-session-input.ts +++ b/services/cloud-agent-next/src/session/control-plane-session-input.ts @@ -1,3 +1,4 @@ +import { byocVercelOwnerFields, VERCEL_BYOC_REQUIRED_MESSAGE } from '../byoc/vercel-owner.js'; /** * Worker-side projections from the public grouped create/send inputs onto the * control-plane V2 Durable Object RPCs. @@ -168,6 +169,21 @@ export function buildControlPlaneSandboxSelection( const provider = metadata.workspace?.sandboxProvider ?? 'cloudflare'; const allocation = metadata.workspace?.sandboxAllocation; const sandboxKind = metadata.workspace?.sandboxKind; + const binding = metadata.workspace?.sandboxProviderBinding; + const byoc = + binding?.kind === 'vercel' && binding.source.kind === 'byoc' + ? { ...byocVercelOwnerFields(binding.source), credentialId: binding.source.credentialId } + : undefined; + if (provider === 'vercel') { + if (!byoc) throw new ControlPlaneRegistrationError(VERCEL_BYOC_REQUIRED_MESSAGE); + const resources = getSandboxAllocationResources(allocation ?? 'vercel-small'); + return { + provider, + configuration: resources === undefined ? { provider } : { provider, resources }, + byoc, + ...(sandboxKind ? { sandboxKind } : {}), + }; + } const enforcementRequested = isCloudAgentContainerBillingEnabled(env, metadata.identity); const billing = { ...buildSandboxBillingInput(metadata, sandboxId, enforcementRequested), @@ -184,14 +200,6 @@ export function buildControlPlaneSandboxSelection( : { egressAllowlist: metadata.workspace.egressAllowlist }), }; switch (provider) { - case 'vercel': { - const resources = getSandboxAllocationResources(allocation ?? 'vercel-small'); - return { - provider, - configuration: resources === undefined ? { provider } : { provider, resources }, - ...pinned, - }; - } case 'cloudflare-containers': { const instance = sandboxKind === 'code-review' diff --git a/services/cloud-agent-next/src/session/safe-failure-projection.test.ts b/services/cloud-agent-next/src/session/safe-failure-projection.test.ts index 4a8e5524134..35a09110109 100644 --- a/services/cloud-agent-next/src/session/safe-failure-projection.test.ts +++ b/services/cloud-agent-next/src/session/safe-failure-projection.test.ts @@ -8,6 +8,9 @@ import { } from '@kilocode/worker-utils/cloud-agent-failure'; import { describe, expect, it } from 'vitest'; import { assistantErrorDetail } from '../shared/assistant-failure.js'; +import type { AssistantMessage } from '@kilocode/sdk/v2'; +import { sdkAssistantErrorSchema } from '@kilocode/session-ingest-contracts'; +import { projectSafeSdkAssistantError } from '../shared/assistant-failure.js'; import { SAFE_FAILURE_MESSAGE_MAX_LENGTH, SafeFailureProjectionSchema, @@ -251,6 +254,194 @@ describe('projectSafeAssistantError', () => { }); }); +describe('projectSafeSdkAssistantError', () => { + it.each([ + [ + 'ProviderAuthError', + { providerID: 'poison-provider' }, + { + name: 'ProviderAuthError', + data: { providerID: 'unknown', message: 'Assistant request was not authorized' }, + }, + ], + [ + 'UnknownError', + { ref: 'poison-reference' }, + { name: 'UnknownError', data: { message: 'Assistant request failed' } }, + ], + [ + 'MessageOutputLengthError', + {}, + { name: 'MessageOutputLengthError', data: { message: 'The model output limit was reached' } }, + ], + [ + 'MessageAbortedError', + {}, + { name: 'MessageAbortedError', data: { message: 'The message was interrupted by the user' } }, + ], + [ + 'StructuredOutputError', + { retries: 3 }, + { + name: 'StructuredOutputError', + data: { message: 'The model response did not match the required format', retries: 3 }, + }, + ], + [ + 'ContextOverflowError', + {}, + { name: 'ContextOverflowError', data: { message: 'The model context limit was exceeded' } }, + ], + [ + 'ContentFilterError', + {}, + { + name: 'UnknownError', + data: { message: 'The model provider blocked the response under its content policy' }, + }, + ], + [ + 'APIError', + { statusCode: 429, isRetryable: true }, + { + name: 'APIError', + data: { message: 'Assistant request was rate limited', statusCode: 429, isRetryable: true }, + }, + ], + [ + 'APIError', + { statusCode: 402, isRetryable: false }, + { + name: 'APIError', + data: { + message: 'Assistant request failed: insufficient credits', + statusCode: 402, + isRetryable: false, + }, + }, + ], + ] as const)( + 'emits only safe SDK fields for %s without losing classification', + (name, data, expected) => { + for (const prefix of ['', '[BYOK] ']) { + const source = { + name, + message: 'poison-outer-message', + data: { + ...data, + message: `${prefix}Unrecognized failure poison-message`, + responseHeaders: { authorization: 'Bearer poison-header', cookie: 'poison-cookie' }, + responseBody: 'poison-body', + metadata: { token: 'poison-metadata' }, + extra: 'poison-extra', + }, + extra: 'poison-outer-extra', + }; + const safeError: AssistantMessage['error'] = projectSafeSdkAssistantError(source); + + expect(safeError).toEqual({ + ...expected, + data: { ...expected.data, message: `${prefix}${expected.data.message}` }, + }); + expect(sdkAssistantErrorSchema.parse(safeError)).toEqual(safeError); + expect(classifyAssistantFailure(safeError)).toEqual(classifyAssistantFailure(source)); + expect(projectSafeSdkAssistantError(safeError)).toEqual(safeError); + expect(JSON.stringify(safeError)).not.toContain('poison'); + } + } + ); + + it.each([undefined, null, '429', 99, 600, 429.5, NaN, Infinity, -Infinity, {}, []])( + 'drops malformed API status %s without inventing retry semantics', + statusCode => { + const safeError = projectSafeSdkAssistantError({ + name: 'APIError', + data: { message: 'poison-message', statusCode, isRetryable: false }, + }); + + expect(safeError).toEqual({ + name: 'APIError', + data: { message: 'Assistant request failed', isRetryable: false }, + }); + expect(sdkAssistantErrorSchema.parse(safeError)).toEqual(safeError); + } + ); + + it.each([undefined, null, 'true', 1, {}, []])( + 'falls back safely for invalid API retryability %j while retaining the failure cause', + isRetryable => { + const safeError = projectSafeSdkAssistantError({ + name: 'APIError', + data: { message: 'poison-message', statusCode: 429, isRetryable }, + }); + + expect(safeError).toEqual({ + name: 'UnknownError', + data: { message: 'Assistant request was rate limited' }, + }); + expect(sdkAssistantErrorSchema.parse(safeError)).toEqual(safeError); + } + ); + + it.each([undefined, null, '3', -1, 0.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1])( + 'does not forward invalid structured-output retry counts %s', + retries => { + const safeError = projectSafeSdkAssistantError({ + name: 'StructuredOutputError', + data: { message: 'poison-message', retries }, + }); + + expect(safeError).toEqual({ + name: 'UnknownError', + data: { message: 'The model response did not match the required format' }, + }); + expect(sdkAssistantErrorSchema.parse(safeError)).toEqual(safeError); + } + ); + + it.each([ + false, + 0, + 'poison-message', + [], + {}, + { + name: 'FutureError', + data: { message: 'poison-message', statusCode: 402, isRetryable: true }, + }, + { name: ['APIError'], data: { statusCode: 402, isRetryable: true } }, + { name: 'APIError', data: null }, + { name: 'APIError', data: [{ statusCode: 402, isRetryable: true }] }, + { name: 'APIError', data: 'poison-message' }, + ])('normalizes unknown or malformed input into a safe SDK error: %j', source => { + const safeError = projectSafeSdkAssistantError(source); + + expect(safeError).toEqual({ + name: 'UnknownError', + data: { message: 'Assistant request failed' }, + }); + expect(sdkAssistantErrorSchema.parse(safeError)).toEqual(safeError); + }); + + it.each(['user-interrupt poison-message', { name: 'MessageAbortedError', data: null }])( + 'preserves interruption in a valid envelope even for incomplete SDK input: %j', + source => { + const safeError = projectSafeSdkAssistantError(source); + + expect(safeError).toEqual({ + name: 'MessageAbortedError', + data: { message: 'The message was interrupted by the user' }, + }); + expect(sdkAssistantErrorSchema.parse(safeError)).toEqual(safeError); + expect(isAssistantInterrupt(safeError)).toBe(true); + } + ); + + it.each([null, undefined])('omits absent SDK errors: %s', source => { + expect(projectSafeSdkAssistantError(source)).toBeUndefined(); + }); +}); + describe('projectSafeFailure', () => { it('projects structured fields while omitting raw failure text', () => { const durableState = { diff --git a/services/cloud-agent-next/src/session/sandbox-allocation-resolution.test.ts b/services/cloud-agent-next/src/session/sandbox-allocation-resolution.test.ts index dda1f5c1ae6..0cca907720c 100644 --- a/services/cloud-agent-next/src/session/sandbox-allocation-resolution.test.ts +++ b/services/cloud-agent-next/src/session/sandbox-allocation-resolution.test.ts @@ -21,7 +21,7 @@ const owner = { userId: 'oauth/user', orgId: 'org-id' }; // Selection is enabled and Vercel is configured. Cloudflare containers are not // enrolled, and credential containment is on by default. -const selectionEnv = { +const selectionEnv = Object.assign({} as Env, { SANDBOX_SELECTION_IDS: 'org-id', VERCEL_TOKEN: 'test-token', VERCEL_TEAM_ID: 'team-id', @@ -31,7 +31,7 @@ const selectionEnv = { VERCEL_SANDBOX_RUNTIME: 'node24', VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: '300000', VERCEL_SANDBOX_EXTEND_DURATION_MS: '600000', -} satisfies Partial; +}); const env = selectionEnv as Env; const enrolledEnv = { ...selectionEnv, CLOUDFLARE_CONTAINERS_ORG_IDS: 'org-id' } as Env; const withoutVercelEnv = { SANDBOX_SELECTION_IDS: 'org-id' } as Env; @@ -117,7 +117,7 @@ describe('resolveSavedSandboxAllocation', () => { contextAllocation: 'vercel-small', }); - await expect(resolve()).resolves.toBe('vercel-small'); + await expect(resolve()).resolves.toBeUndefined(); }); it('skips a saved Vercel choice for a GitLab repository', async () => { @@ -143,10 +143,12 @@ describe('resolveSavedSandboxAllocation', () => { } ); - it('keeps a saved Vercel choice when nothing restricts it', async () => { + it('keeps a saved Vercel choice only for a BYOC-enrolled owner', async () => { getSandboxAllocationCandidatesMock.mockResolvedValue({ contextAllocation: 'vercel-small' }); - await expect(resolve()).resolves.toBe('vercel-small'); + await expect(resolve({ env: { ...env, BYOC_VERCEL_IDS: owner.orgId } as Env })).resolves.toBe( + 'vercel-small' + ); }); it('returns nothing when no saved setting resolves', async () => { diff --git a/services/cloud-agent-next/src/session/sandbox-allocation-resolution.ts b/services/cloud-agent-next/src/session/sandbox-allocation-resolution.ts index 0514554c0a9..f622fd0478d 100644 --- a/services/cloud-agent-next/src/session/sandbox-allocation-resolution.ts +++ b/services/cloud-agent-next/src/session/sandbox-allocation-resolution.ts @@ -7,6 +7,7 @@ import { } from '@kilocode/worker-utils/sandbox-allocation'; import { getSandboxSelectionCapabilities, + isByocIncompatibleAllocation, isSandboxAllocationAvailable, type SandboxAllocationCapability, type SessionRepositoryType, @@ -71,7 +72,11 @@ export async function resolveSavedSandboxAllocation(args: { ]) { if (candidate === undefined) continue; const allocation = candidate as SandboxAllocation; - if (!isSelectableSandboxAllocation(allocation)) continue; + if ( + !isSelectableSandboxAllocation(allocation) || + isByocIncompatibleAllocation(env, owner, allocation) + ) + continue; // The same check as the picker, so a saved choice that the picker offered is // never skipped here. if (isSandboxAllocationAvailable(capabilities, allocation)) return allocation; diff --git a/services/cloud-agent-next/src/session/session-prepare.test.ts b/services/cloud-agent-next/src/session/session-prepare.test.ts index 6a1c3b28330..053c9fdd6e0 100644 --- a/services/cloud-agent-next/src/session/session-prepare.test.ts +++ b/services/cloud-agent-next/src/session/session-prepare.test.ts @@ -36,6 +36,7 @@ import type { SessionMessageAdmissionResult } from '../execution/types.js'; import type { SessionCreateRequest } from './session-requests.js'; import type * as SandboxIdModule from '../sandbox-id.js'; import type * as SessionPlaneModule from '../session-identity.js'; +import type * as ByocCredentialResolverModule from '../byoc/vercel-credential-resolver.js'; import type * as SharedSandboxRouteModule from '../shared-sandbox-route.js'; import type * as SandboxPlacementModule from '../sandbox-placement.js'; import type * as MessageIdModule from './message-id.js'; @@ -73,6 +74,8 @@ const { generateSandboxRoutingTargetMock, getSandboxAllocationCandidatesMock, selectControlPlaneSandboxMock, + fetchByocVercelEnrollmentMock, + fetchByocVercelEnrollmentForUserMock, } = vi.hoisted(() => ({ admitOperationMock: vi.fn(), settleOperationMock: vi.fn().mockResolvedValue({ settled: true }), @@ -89,6 +92,8 @@ const { generateSandboxRoutingTargetMock: vi.fn(), getSandboxAllocationCandidatesMock: vi.fn().mockResolvedValue({}), selectControlPlaneSandboxMock: vi.fn(), + fetchByocVercelEnrollmentMock: vi.fn(), + fetchByocVercelEnrollmentForUserMock: vi.fn(), })); vi.mock('@kilocode/worker-utils/runtime-authorization', () => ({ @@ -179,6 +184,15 @@ vi.mock('../sandbox-placement.js', async importOriginal => { return { ...actual, selectControlPlaneSandbox: selectControlPlaneSandboxMock }; }); +vi.mock('../byoc/vercel-credential-resolver.js', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + fetchByocVercelEnrollment: fetchByocVercelEnrollmentMock, + fetchByocVercelEnrollmentForUser: fetchByocVercelEnrollmentForUserMock, + }; +}); + vi.mock('../shared-sandbox-route.js', async importOriginal => { const actual = await importOriginal(); return { @@ -207,6 +221,7 @@ const PLACED_SANDBOX_DECISION = { sandboxKind: 'isolated', } as const; const WORKTREE_ID = 'worktree_420ae020-e3c4-4e67-878b-66672c3d997e'; +const CONTROL_CLOUD_AGENT_SESSION_ID = 'workspace_12345678-1234-1234-1234-123456789abc'; const KILO_SESSION_ID = 'ses_12345678901234567890123456'; const INITIAL_MESSAGE_ID = 'msg_018f1e2d3c4bAbCdEfGhIjKlMn'; const ROW_ID = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'; @@ -474,6 +489,8 @@ describe('explicit sandbox session creation', () => { beforeEach(async () => { vi.clearAllMocks(); + admitOperationMock.mockReset(); + fetchByocVercelEnrollmentMock.mockReset(); getPgDbMock.mockReturnValue( makeDb([[{ id: 'member' }], [{ email: 'test@example.com' }], [{ id: 'member' }]]) ); @@ -519,7 +536,16 @@ describe('explicit sandbox session creation', () => { const doStub = makeDoStub(); const request = requestForPreset(sandboxAllocation); request.runtime = { sandboxAllocation, egressMode: 'allowlist' }; - await expect(runCreate(selectedContext(doStub), request)).rejects.toMatchObject({ + const ctx = selectedContext(doStub); + if (sandboxAllocation === 'vercel-small') { + ctx.env.BYOC_VERCEL_IDS = orgId; + fetchByocVercelEnrollmentMock.mockResolvedValueOnce({ + organizationId: orgId, + credentialId: 'f47ac10b-58cc-4372-a567-0e02b2c3d480', + setupStatus: 'ready', + }); + } + await expect(runCreate(ctx, request)).rejects.toMatchObject({ code: 'BAD_REQUEST', }); expect(createCliSessionMock).not.toHaveBeenCalled(); @@ -643,7 +669,7 @@ describe('explicit sandbox session creation', () => { expectNoAllocation(doStub); }); - it.each(['vercel-small', 'cloudflare-shared', 'isolated-standard'] as const)( + it.each(['cloudflare-shared', 'isolated-standard'] as const)( 'allows explicit unrestricted mode on %s', async sandboxAllocation => { const doStub = makeDoStub(); @@ -772,17 +798,19 @@ describe('explicit sandbox session creation', () => { }); it.each( - SELECTABLE_SANDBOX_ALLOCATIONS.flatMap(preset => [ - { preset, format: 'legacy' }, - { preset, format: 'structured' }, - ]) + SELECTABLE_SANDBOX_ALLOCATIONS.filter(allocation => !allocation.startsWith('vercel-')).flatMap( + preset => [ + { preset, format: 'legacy' }, + { preset, format: 'structured' }, + ] + ) )( 'persists $format $preset with its canonical allocation and valid metadata', async ({ preset, format }) => { const doStub = makeDoStub(); const ctx = selectedContext(doStub); if (preset.startsWith('vercel-')) { - ctx.env.VERCEL_SANDBOX_ORG_IDS = ''; + Object.assign(ctx.env, { VERCEL_SANDBOX_ORG_IDS: '' }); ctx.env.PER_SESSION_SANDBOX_ORG_IDS = ''; } await runCreate( @@ -833,24 +861,24 @@ describe('explicit sandbox session creation', () => { ); it.each(['vercel-small', 'vercel-large'] as const)( - 'forces the control plane for %s even when the owner is not enrolled', + 'rejects %s when the owner has no Vercel BYOC enrollment', async preset => { const doStub = makeDoStub(); const ctx = selectedContext(doStub); - ctx.env.VERCEL_SANDBOX_ORG_IDS = ''; - await runCreate(ctx, requestForPreset(preset)); - expect(generateSessionIdMock).toHaveBeenCalledWith(); - const command = createdMetadata(doStub); - expect(command?.workspace).toMatchObject({ - sandboxAllocation: preset, - sandboxProvider: 'vercel', - sandboxKind: 'isolated', + getPgDbMock.mockReturnValue(makeDb([[{ id: 'member' }], []])); + await expect(runCreate(ctx, requestForPreset(preset))).rejects.toMatchObject({ + code: 'BAD_REQUEST', + message: expect.stringContaining('Configure Vercel BYOC'), }); - expect(command?.workspace?.sandboxId).toMatch(/^sbx-/); + expectNoAllocation(doStub); } ); - it.each(SELECTABLE_SANDBOX_ALLOCATIONS)( + it.each( + SELECTABLE_SANDBOX_ALLOCATIONS.filter(allocation => !allocation.startsWith('vercel-')).filter( + allocation => !allocation.startsWith('vercel-') + ) + )( 'recovers %s on same-key takeover after membership lookup fails before progress', async sandboxAllocation => { const request = requestForPreset(sandboxAllocation); @@ -977,7 +1005,11 @@ describe('explicit sandbox session creation', () => { } ); - it.each(SELECTABLE_SANDBOX_ALLOCATIONS)( + it.each( + SELECTABLE_SANDBOX_ALLOCATIONS.filter(allocation => !allocation.startsWith('vercel-')).filter( + allocation => !allocation.startsWith('vercel-') + ) + )( 'creates the first worktree chat with %s and upstream finalization/containment semantics', async sandboxAllocation => { const doStub = makeDoStub(); @@ -1081,6 +1113,14 @@ describe('explicit sandbox session creation', () => { initialMessageId: INITIAL_MESSAGE_ID, sandboxId: `ses-${'a'.repeat(48)}`, sandboxProvider: 'vercel', + sandboxProviderBinding: { + kind: 'vercel', + source: { + kind: 'byoc', + organizationId: orgId, + credentialId: '22222222-2222-4222-8222-222222222222', + }, + }, sandboxAllocation: 'vercel-large', [SESSION_CREATE_WORKTREE_ENABLED_KEY]: true, [SESSION_CREATE_FINALIZATION_VERSION_KEY]: finalizationVersion, @@ -1154,7 +1194,7 @@ describe('explicit sandbox session creation', () => { ); }); - it.each(SELECTABLE_SANDBOX_ALLOCATIONS)( + it.each(SELECTABLE_SANDBOX_ALLOCATIONS.filter(allocation => !allocation.startsWith('vercel-')))( 'replays a structured %s request against a legacy stored fingerprint', async sandboxAllocation => { const legacyRequest = requestForPreset(sandboxAllocation); @@ -1174,7 +1214,7 @@ describe('explicit sandbox session creation', () => { const doStub = makeDoStub(); const ctx = selectedContext(doStub); ctx.env.SANDBOX_SELECTION_IDS = ''; - ctx.env.VERCEL_TOKEN = ''; + Object.assign(ctx.env, { VERCEL_TOKEN: '' }); getPgDbMock.mockReturnValue(makeDb([[{ id: 'member' }], [row]])); admitOperationMock.mockResolvedValue({ admission: 'duplicate_settled', row }); await expect(runCreate(ctx, request)).resolves.toEqual({ @@ -1201,7 +1241,7 @@ describe('explicit sandbox session creation', () => { const doStub = makeDoStub(); const ctx = selectedContext(doStub); ctx.env.SANDBOX_SELECTION_IDS = ''; - ctx.env.VERCEL_TOKEN = ''; + Object.assign(ctx.env, { VERCEL_TOKEN: '' }); getPgDbMock.mockReturnValue(makeDb([[{ id: 'member' }], [row]])); admitOperationMock.mockResolvedValue({ admission: 'duplicate_settled', row }); await expect(runCreate(ctx, request)).resolves.toEqual({ @@ -1212,7 +1252,7 @@ describe('explicit sandbox session creation', () => { expectNoAllocation(doStub); }); - it.each(['vercel-small', 'cloudflare-single', undefined] as const)( + it.each(['cloudflare-containers-standard-4', 'cloudflare-single', undefined] as const)( 'rejects a same-key replay changing or removing the preset to %s', async sandboxAllocation => { const row = makeLedgerRow({ @@ -1221,16 +1261,16 @@ describe('explicit sandbox session creation', () => { canonical_result: { cloudAgentSessionId: WORKSPACE_SESSION_ID, kiloSessionId: KILO_SESSION_ID, - sandboxAllocation: 'vercel-large', + sandboxAllocation: 'cloudflare-shared', [SESSION_CREATE_INTENT_FINGERPRINT_KEY]: await sessionCreateIntentFingerprint( - requestForPreset('vercel-large') + requestForPreset('cloudflare-shared') ), }, }); const doStub = makeDoStub(); admitOperationMock.mockResolvedValue({ admission: 'duplicate_settled', row }); const request = { - ...requestForPreset('vercel-large'), + ...requestForPreset('cloudflare-shared'), runtime: sandboxAllocation ? { sandboxAllocation } : undefined, }; await expect(runCreate(selectedContext(doStub), request)).rejects.toThrow( @@ -1322,6 +1362,14 @@ describe('explicit sandbox session creation', () => { kiloSessionId: KILO_SESSION_ID, sandboxId: `ses-${'a'.repeat(48)}`, sandboxProvider: 'vercel', + sandboxProviderBinding: { + kind: 'vercel', + source: { + kind: 'byoc', + organizationId: orgId, + credentialId: '22222222-2222-4222-8222-222222222222', + }, + }, sandboxAllocation: 'vercel-large', [SESSION_CREATE_INTENT_FINGERPRINT_KEY]: await sessionCreateIntentFingerprint(request), }, @@ -1370,7 +1418,7 @@ describe('explicit sandbox session creation', () => { getPgDbMock.mockReturnValue(makeDb([[{ id: 'member' }], []])); admitOperationMock.mockResolvedValue({ admission: 'takeover', row }); await expect(runCreate(selectedContext(doStub), request)).rejects.toThrow( - 'creation_in_progress' + 'Configure Vercel BYOC' ); expectNoAllocation(doStub); }); @@ -1516,7 +1564,7 @@ describe('explicit sandbox session creation', () => { await expect(runCreate(ctx, request)).rejects.toMatchObject({ code: 'BAD_REQUEST', - message: 'Vercel sandboxes support GitHub repositories only', + message: expect.stringContaining('Configure Vercel BYOC'), }); expect(admitOperationMock).not.toHaveBeenCalled(); expect(recordOperationProgressMock).not.toHaveBeenCalled(); @@ -1846,6 +1894,7 @@ describe('sessionCreateIntentFingerprint', () => { describe('createSessionWithLedger admission ladder', () => { beforeEach(() => { vi.clearAllMocks(); + fetchByocVercelEnrollmentMock.mockReset(); getPgDbMock.mockReturnValue(makeDb([[{ email: 'test@example.com' }]])); generateSessionIdMock.mockReturnValue(SANDBOX_SESSION_ID); generateKiloSessionIdMock.mockReturnValue(KILO_SESSION_ID); @@ -2486,7 +2535,383 @@ describe('createSessionWithLedger admission ladder', () => { ); }); + it.each(['f47ac10b-58cc-4372-a567-0e02b2c3d480', '*'])( + 'rejects isolated Standard allocation for BYOC allowlist %s before external effects', + async byocOrgIds => { + const doStub = makeDoStub(); + const ctx = makeContext(doStub); + Object.assign(ctx.env, { BYOC_VERCEL_IDS: byocOrgIds, CONTROL_PLANE_IDS: '' }); + + await expect( + runCreate( + ctx, + makeRequest({ + runtime: { sandboxAllocation: 'isolated-standard' }, + options: { + operationKey: OPERATION_KEY, + kilocodeOrganizationId: 'f47ac10b-58cc-4372-a567-0e02b2c3d480', + }, + }) + ) + ).rejects.toMatchObject({ + code: 'BAD_REQUEST', + message: 'Sandbox allocation is not supported for BYOC Vercel accounts', + }); + + expect(admitOperationMock).not.toHaveBeenCalled(); + expect(generateSessionIdMock).not.toHaveBeenCalled(); + expect(fetchByocVercelEnrollmentMock).not.toHaveBeenCalled(); + expect(createCliSessionMock).not.toHaveBeenCalled(); + expect(doStub.createSessionWithInitialAdmission).not.toHaveBeenCalled(); + } + ); + + it.each(['f47ac10b-58cc-4372-a567-0e02b2c3d480', '*'])( + 'rejects Cloudflare allocations for BYOC allowlist %s before external effects', + async byocOrgIds => { + const doStub = makeDoStub(); + const ctx = makeContext(doStub); + Object.assign(ctx.env, { BYOC_VERCEL_IDS: byocOrgIds, CONTROL_PLANE_IDS: '' }); + + await expect( + runCreate( + ctx, + makeRequest({ + runtime: { sandboxAllocation: 'cloudflare-single' }, + options: { + operationKey: OPERATION_KEY, + kilocodeOrganizationId: 'f47ac10b-58cc-4372-a567-0e02b2c3d480', + }, + }) + ) + ).rejects.toMatchObject({ + code: 'BAD_REQUEST', + message: 'Sandbox allocation is not supported for BYOC Vercel accounts', + }); + + expect(admitOperationMock).not.toHaveBeenCalled(); + expect(generateSessionIdMock).not.toHaveBeenCalled(); + expect(fetchByocVercelEnrollmentMock).not.toHaveBeenCalled(); + expect(createCliSessionMock).not.toHaveBeenCalled(); + expect(doStub.createSessionWithInitialAdmission).not.toHaveBeenCalled(); + } + ); + + it.each([ + { type: 'gitlab', url: 'https://gitlab.com/acme/repo.git' }, + { + type: 'bitbucket', + url: 'https://bitbucket.org/acme/repo.git', + workspaceUuid: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + repositoryUuid: '6ba7b810-9dad-41d1-80b4-00c04fd430c8', + }, + { type: 'git', url: 'https://example.com/acme/repo.git' }, + ] as const)('rejects implicit BYOC compute for $type before admission', async repository => { + const ctx = makeContext(makeDoStub()); + Object.assign(ctx.env, { BYOC_VERCEL_IDS: USER_ID, SANDBOX_SELECTION_IDS: '' }); + await expect(runCreate(ctx, makeRequest({ repository }))).rejects.toMatchObject({ + code: 'BAD_REQUEST', + message: 'Vercel sandboxes support GitHub repositories only', + }); + expect(admitOperationMock).not.toHaveBeenCalled(); + expect(fetchByocVercelEnrollmentMock).not.toHaveBeenCalled(); + expect(createCliSessionMock).not.toHaveBeenCalled(); + }); + + it('rejects implicit BYOC compute for security remediation before admission', async () => { + const ctx = makeContext(makeDoStub()); + Object.assign(ctx.env, { BYOC_VERCEL_IDS: USER_ID, SANDBOX_SELECTION_IDS: '' }); + await expect( + runCreate( + ctx, + makeRequest({ + options: { operationKey: OPERATION_KEY, createdOnPlatform: 'security-remediation' }, + }) + ) + ).rejects.toMatchObject({ + code: 'BAD_REQUEST', + message: 'Vercel sandboxes do not include the security-remediation command-line tools', + }); + expect(admitOperationMock).not.toHaveBeenCalled(); + expect(fetchByocVercelEnrollmentMock).not.toHaveBeenCalled(); + }); + + it('allocates BYOC sessions on the control plane before recording session identity', async () => { + const organizationId = 'f47ac10b-58cc-4372-a567-0e02b2c3d480'; + admitOperationMock.mockResolvedValueOnce({ + admission: 'admitted', + row: makeLedgerRow({ organization_id: organizationId }), + }); + const credentialId = 'f47ac10b-58cc-4372-a567-0e02b2c3d481'; + const doStub = makeDoStub(); + const ctx = makeContext(doStub); + const getControlSession = vi.spyOn(ctx.env.SANDBOX_SESSION, 'get'); + Object.assign(ctx.env, { + BYOC_VERCEL_IDS: organizationId, + CONTROL_PLANE_IDS: '', + }); + generateSessionIdMock.mockReturnValueOnce(CONTROL_CLOUD_AGENT_SESSION_ID); + selectControlPlaneSandboxMock.mockResolvedValueOnce(PLACED_SANDBOX_DECISION); + fetchByocVercelEnrollmentMock.mockResolvedValueOnce({ + organizationId, + credentialId, + setupStatus: 'ready', + }); + + const result = await runCreate( + ctx, + makeRequest({ + options: { operationKey: OPERATION_KEY, kilocodeOrganizationId: organizationId }, + }) + ); + + expect(generateSessionIdMock).toHaveBeenCalledWith(); + expect(recordOperationProgressMock).toHaveBeenNthCalledWith(1, expect.any(Object), ROW_ID, { + cloudAgentSessionId: CONTROL_CLOUD_AGENT_SESSION_ID, + kiloSessionId: KILO_SESSION_ID, + initialMessageId: INITIAL_MESSAGE_ID, + [SESSION_CREATE_WORKTREE_ENABLED_KEY]: false, + [SESSION_CREATE_FINALIZATION_VERSION_KEY]: 2, + createIntentFingerprint: expect.any(String), + }); + expect(createSessionReportMock).toHaveBeenCalledWith( + { + cloudAgentSessionId: CONTROL_CLOUD_AGENT_SESSION_ID, + kiloSessionId: KILO_SESSION_ID, + initialMessageId: INITIAL_MESSAGE_ID, + }, + ctx.env + ); + expect(fetchByocVercelEnrollmentMock).toHaveBeenCalledWith(ctx.env, organizationId); + expect(selectControlPlaneSandboxMock).toHaveBeenCalledWith({ + env: ctx.env, + sessionId: CONTROL_CLOUD_AGENT_SESSION_ID, + userId: USER_ID, + orgId: organizationId, + botId: undefined, + codeReview: false, + sandboxAllocation: undefined, + }); + expect(recordOperationProgressMock).toHaveBeenNthCalledWith(2, expect.any(Object), ROW_ID, { + sandboxId: PLACED_SANDBOX_ID, + sandboxProvider: 'vercel', + sandboxKind: 'isolated', + sandboxProviderBinding: { + kind: 'vercel', + source: { kind: 'byoc', organizationId, credentialId }, + }, + }); + expect(recordSandboxIdentityMock).toHaveBeenCalledWith( + { + cloudAgentSessionId: CONTROL_CLOUD_AGENT_SESSION_ID, + sandboxId: PLACED_SANDBOX_ID, + }, + ctx.env + ); + expect(createCliSessionMock).toHaveBeenCalledWith( + expect.objectContaining({ + sessionId: KILO_SESSION_ID, + cloudAgentSessionId: CONTROL_CLOUD_AGENT_SESSION_ID, + kiloUserId: USER_ID, + organizationId, + createdOnPlatform: 'cloud-agent', + gitUrl: 'https://github.com/acme/repo', + }) + ); + expect(getControlSession).toHaveBeenCalledOnce(); + expect(doStub.createSessionWithInitialAdmission).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + identity: expect.objectContaining({ sessionId: CONTROL_CLOUD_AGENT_SESSION_ID }), + workspace: expect.objectContaining({ + sandboxId: PLACED_SANDBOX_ID, + sandboxProvider: 'vercel', + sandboxProviderBinding: { + kind: 'vercel', + source: { kind: 'byoc', organizationId, credentialId }, + }, + }), + }), + }) + ); + expect(result).toEqual({ + cloudAgentSessionId: CONTROL_CLOUD_AGENT_SESSION_ID, + kiloSessionId: KILO_SESSION_ID, + }); + }); + + it('keeps BYOC-enrolled Code Reviewer sessions on their existing Cloudflare plane', async () => { + const organizationId = 'f47ac10b-58cc-4372-a567-0e02b2c3d482'; + admitOperationMock.mockResolvedValueOnce({ + admission: 'admitted', + row: makeLedgerRow({ organization_id: organizationId }), + }); + const doStub = makeDoStub(); + const ctx = makeContext(doStub); + const getControlSession = vi.spyOn(ctx.env.SANDBOX_SESSION, 'get'); + Object.assign(ctx.env, { + BYOC_VERCEL_IDS: organizationId, + CONTROL_PLANE_IDS: '', + }); + selectControlPlaneSandboxMock.mockResolvedValueOnce({ + ...PLACED_SANDBOX_DECISION, + sandboxKind: 'code-review', + provider: 'cloudflare', + }); + const request = makeRequest({ + options: { + operationKey: OPERATION_KEY, + kilocodeOrganizationId: organizationId, + createdOnPlatform: 'code-review', + }, + }); + + const result = await createSessionWithLedger(request, ctx, { + ...CREATE_OPTIONS, + billingOrigin: 'code-review', + }); + + expect(generateSessionIdMock).toHaveBeenCalledWith(); + expect(fetchByocVercelEnrollmentMock).not.toHaveBeenCalled(); + expect(recordOperationProgressMock).toHaveBeenNthCalledWith( + 1, + expect.any(Object), + ROW_ID, + expect.objectContaining({ cloudAgentSessionId: SANDBOX_SESSION_ID }) + ); + expect(recordOperationProgressMock).toHaveBeenNthCalledWith(2, expect.any(Object), ROW_ID, { + sandboxId: PLACED_SANDBOX_ID, + sandboxProvider: 'cloudflare', + sandboxKind: 'code-review', + sandboxProviderBinding: { kind: 'cloudflare' }, + }); + expect(getControlSession).toHaveBeenCalledOnce(); + expect(doStub.createSessionWithInitialAdmission).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + identity: expect.objectContaining({ + sessionId: SANDBOX_SESSION_ID, + billingOrigin: 'code-review', + }), + workspace: expect.objectContaining({ + sandboxId: PLACED_SANDBOX_ID, + sandboxProvider: 'cloudflare', + sandboxProviderBinding: { kind: 'cloudflare' }, + }), + }), + }) + ); + expect(result).toEqual({ + cloudAgentSessionId: SANDBOX_SESSION_ID, + kiloSessionId: KILO_SESSION_ID, + }); + }); + + it('rejects unsupported BYOC review tools from an untrusted platform label', async () => { + const organizationId = 'f47ac10b-58cc-4372-a567-0e02b2c3d483'; + const ctx = makeContext(makeDoStub()); + Object.assign(ctx.env, { BYOC_VERCEL_IDS: organizationId, CONTROL_PLANE_IDS: '' }); + await expect( + createSessionWithLedger( + makeRequest({ + options: { + operationKey: OPERATION_KEY, + kilocodeOrganizationId: organizationId, + createdOnPlatform: 'code-review', + }, + }), + ctx, + { ...CREATE_OPTIONS, billingOrigin: 'cloud-agent' } + ) + ).rejects.toMatchObject({ + code: 'BAD_REQUEST', + message: 'Vercel sandboxes do not include the code-review publishing tools', + }); + expect(admitOperationMock).not.toHaveBeenCalled(); + expect(generateSessionIdMock).not.toHaveBeenCalled(); + expect(fetchByocVercelEnrollmentMock).not.toHaveBeenCalled(); + }); + + it('enrolls personal sessions when the BYOC allowlist contains a wildcard', async () => { + const doStub = makeDoStub(); + const ctx = makeContext(doStub); + Object.assign(ctx.env, { BYOC_VERCEL_IDS: '*', CONTROL_PLANE_IDS: '' }); + const credentialId = 'f47ac10b-58cc-4372-a567-0e02b2c3d486'; + fetchByocVercelEnrollmentForUserMock.mockResolvedValueOnce({ + userId: USER_ID, + credentialId, + setupStatus: 'ready', + }); + generateSessionIdMock.mockReturnValueOnce(CONTROL_CLOUD_AGENT_SESSION_ID); + selectControlPlaneSandboxMock.mockResolvedValueOnce(PLACED_SANDBOX_DECISION); + + await runCreate(ctx); + + expect(generateSessionIdMock).toHaveBeenCalledWith(); + expect(fetchByocVercelEnrollmentMock).not.toHaveBeenCalled(); + expect(fetchByocVercelEnrollmentForUserMock).toHaveBeenCalledWith(ctx.env, USER_ID); + expect(selectControlPlaneSandboxMock).toHaveBeenCalledWith({ + env: ctx.env, + sessionId: CONTROL_CLOUD_AGENT_SESSION_ID, + userId: USER_ID, + orgId: undefined, + botId: undefined, + codeReview: false, + sandboxAllocation: undefined, + }); + expect(recordOperationProgressMock).toHaveBeenNthCalledWith(2, expect.any(Object), ROW_ID, { + sandboxId: PLACED_SANDBOX_ID, + sandboxProvider: 'vercel', + sandboxKind: 'isolated', + sandboxProviderBinding: { + kind: 'vercel', + source: { kind: 'byoc', userId: USER_ID, credentialId }, + }, + }); + expect(createCliSessionMock).toHaveBeenCalled(); + expect(doStub.createSessionWithInitialAdmission).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + workspace: expect.objectContaining({ + sandboxId: PLACED_SANDBOX_ID, + sandboxProvider: 'vercel', + sandboxProviderBinding: { + kind: 'vercel', + source: { kind: 'byoc', userId: USER_ID, credentialId }, + }, + }), + }), + }) + ); + }); + + it('rejects isolated Standard allocation for an enrolled personal BYOC session', async () => { + const ctx = makeContext(makeDoStub()); + Object.assign(ctx.env, { BYOC_VERCEL_IDS: USER_ID, CONTROL_PLANE_IDS: '' }); + + await expect( + runCreate( + ctx, + makeRequest({ + runtime: { sandboxAllocation: 'isolated-standard' }, + options: { operationKey: OPERATION_KEY }, + }) + ) + ).rejects.toMatchObject({ + code: 'BAD_REQUEST', + message: 'Sandbox allocation is not supported for BYOC Vercel accounts', + }); + + expect(admitOperationMock).not.toHaveBeenCalled(); + expect(generateSessionIdMock).not.toHaveBeenCalled(); + expect(fetchByocVercelEnrollmentForUserMock).not.toHaveBeenCalled(); + }); + it('passes a normalized GitHub repository URL to the session-ingest create call', async () => { + admitOperationMock.mockResolvedValueOnce({ + admission: 'admitted', + row: makeLedgerRow({ resource_key: null }), + }); const ctx = makeContext(makeDoStub()); await runCreate( @@ -2897,6 +3322,7 @@ describe('createSessionWithLedger admission ladder', () => { describe('createSessionWithLedger takeover reconciliation ladder', () => { beforeEach(() => { vi.clearAllMocks(); + fetchByocVercelEnrollmentMock.mockReset(); generateSessionIdMock.mockReturnValue(SANDBOX_SESSION_ID); generateKiloSessionIdMock.mockReturnValue(KILO_SESSION_ID); generateSandboxRoutingTargetMock.mockResolvedValue({ @@ -3655,6 +4081,7 @@ describe('createSessionWithLedger worktree and ownership reconciliation', () => beforeEach(() => { vi.clearAllMocks(); + fetchByocVercelEnrollmentMock.mockReset(); getPgDbMock.mockReturnValue(makeDb([[{ email: 'test@example.com' }]])); generateSessionIdMock.mockReturnValue(WORKSPACE_SESSION_ID); generateKiloSessionIdMock.mockReturnValue(KILO_SESSION_ID); @@ -3801,6 +4228,82 @@ describe('createSessionWithLedger worktree and ownership reconciliation', () => } ); + it('reconciles the original BYOC worktree binding after enrollment changes without reallocating', async () => { + const organizationId = 'f47ac10b-58cc-4372-a567-0e02b2c3d488'; + const credentialId = 'f47ac10b-58cc-4372-a567-0e02b2c3d489'; + const input = request({ + options: { ...request().options, kilocodeOrganizationId: organizationId }, + }); + const doStub = makeDoStub({ getMetadata: vi.fn().mockResolvedValue(null) }); + const ctx = context(doStub); + ctx.env.BYOC_VERCEL_IDS = organizationId; + admitOperationMock.mockResolvedValueOnce({ + admission: 'admitted', + row: makeLedgerRow({ organization_id: organizationId }), + }); + selectControlPlaneSandboxMock.mockResolvedValueOnce(PLACED_SANDBOX_DECISION); + fetchByocVercelEnrollmentMock.mockResolvedValueOnce({ + organizationId, + credentialId, + setupStatus: 'ready', + }); + createCliSessionMock.mockRejectedValueOnce(new Error('ownership response lost after commit')); + + await expect(runCreate(ctx, input)).rejects.toThrow('ownership response lost after commit'); + const storedProgress: Record = Object.assign( + {}, + ...recordOperationProgressMock.mock.calls.map(call => call[2]) + ); + const sandboxProviderBinding = { + kind: 'vercel', + source: { kind: 'byoc', organizationId, credentialId }, + }; + expect(storedProgress).toMatchObject({ + [SESSION_CREATE_WORKTREE_ENABLED_KEY]: true, + sandboxId: PLACED_SANDBOX_ID, + sandboxProvider: 'vercel', + sandboxProviderBinding, + }); + ctx.env.BYOC_VERCEL_IDS = ''; + admitOperationMock.mockResolvedValueOnce({ + admission: 'duplicate_reconcile_pending', + row: makeLedgerRow({ + organization_id: organizationId, + status: 'reconcile_pending', + canonical_result: storedProgress, + }), + }); + getPgDbMock.mockReturnValue( + makeDb([[ownershipRow({ organizationId })], [{ email: 'test@example.com' }]]) + ); + + await expect(runCreate(ctx, input)).resolves.toEqual({ + cloudAgentSessionId: WORKSPACE_SESSION_ID, + kiloSessionId: KILO_SESSION_ID, + replayed: true, + }); + + expect(doStub.createSessionWithInitialAdmission).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + workspace: expect.objectContaining({ + sandboxId: PLACED_SANDBOX_ID, + sandboxProvider: 'vercel', + sandboxProviderBinding, + worktreeId: WORKTREE_ID, + workspacePath: `/workspace/${organizationId}/${USER_ID}/worktrees/${WORKTREE_ID}`, + }), + finalization: input.finalization, + }), + }) + ); + expect(fetchByocVercelEnrollmentMock).toHaveBeenCalledOnce(); + expect(selectControlPlaneSandboxMock).toHaveBeenCalledOnce(); + expect(generateSessionIdMock).toHaveBeenCalledOnce(); + expect(createCliSessionMock).toHaveBeenCalledOnce(); + expect(deleteCliSessionMock).not.toHaveBeenCalled(); + }); + it.each([true, false, undefined])( 'recovers committed ownership with autoCommit=%s without changing the initial turn', async autoCommit => { @@ -3903,6 +4406,7 @@ describe('createSessionWithLedger worktree and ownership reconciliation', () => ...recordOperationProgressMock.mock.calls.map(call => call[2]) ); vi.clearAllMocks(); + fetchByocVercelEnrollmentMock.mockReset(); const db = makeDb([[ownershipRow()]]); const select = vi.spyOn(db, 'select'); getPgDbMock.mockReturnValue(db); @@ -4157,6 +4661,18 @@ describe('createSessionWithLedger worktree and ownership reconciliation', () => canonical_result: await canonicalProgress(input, { sandboxId: storedSandboxId, sandboxProvider: provider, + ...(provider === 'vercel' + ? { + sandboxProviderBinding: { + kind: 'vercel', + source: { + kind: 'byoc', + userId: USER_ID, + credentialId: '22222222-2222-4222-8222-222222222222', + }, + }, + } + : {}), ...(sandboxKind ? { sandboxKind } : {}), }), }), @@ -4321,6 +4837,7 @@ describe('createSessionWithLedger changed-intent rejection', () => { beforeEach(() => { vi.clearAllMocks(); + fetchByocVercelEnrollmentMock.mockReset(); getPgDbMock.mockReturnValue(makeDb([[{ email: 'test@example.com' }]])); generateSessionIdMock.mockReturnValue(SANDBOX_SESSION_ID); generateKiloSessionIdMock.mockReturnValue(KILO_SESSION_ID); @@ -4862,6 +5379,7 @@ describe('createSessionWithLedger clone allocation outcomes', () => { beforeEach(async () => { vi.clearAllMocks(); + fetchByocVercelEnrollmentMock.mockReset(); const actualPlacement = await vi.importActual('../sandbox-placement.js'); selectControlPlaneSandboxMock.mockImplementation(actualPlacement.selectControlPlaneSandbox); @@ -5165,6 +5683,66 @@ describe('createSessionWithLedger clone allocation outcomes', () => { } ); + it('registers BYOC clone-only sessions in the control-plane Durable Object', async () => { + const organizationId = 'f47ac10b-58cc-4372-a567-0e02b2c3d486'; + admitOperationMock.mockResolvedValueOnce({ + admission: 'admitted', + row: makeLedgerRow({ organization_id: organizationId }), + }); + const credentialId = 'f47ac10b-58cc-4372-a567-0e02b2c3d487'; + const doStub = makeDoStub(); + const ctx = makeContext(doStub); + const getControlSession = vi.spyOn(ctx.env.SANDBOX_SESSION, 'get'); + Object.assign(ctx.env, { BYOC_VERCEL_IDS: organizationId, CONTROL_PLANE_IDS: '' }); + generateSessionIdMock.mockReturnValueOnce(CONTROL_CLOUD_AGENT_SESSION_ID); + selectControlPlaneSandboxMock.mockResolvedValueOnce(PLACED_SANDBOX_DECISION); + fetchByocVercelEnrollmentMock.mockResolvedValueOnce({ + organizationId, + credentialId, + setupStatus: 'ready', + }); + createCliSessionMock.mockResolvedValue({ + status: 'ready', + clone: { sessionId: KILO_SESSION_ID, copiedItemCount: 1 }, + }); + const request = makeRequest({ + initialTurn: undefined, + clone: { cloneFromKiloSessionId: SOURCE_KILO_SESSION_ID }, + options: { operationKey: OPERATION_KEY, kilocodeOrganizationId: organizationId }, + }); + + const result = await runCreate(ctx, request); + + expect(generateSessionIdMock).toHaveBeenCalledWith(); + expect(recordOperationProgressMock).toHaveBeenNthCalledWith(1, expect.any(Object), ROW_ID, { + cloudAgentSessionId: CONTROL_CLOUD_AGENT_SESSION_ID, + kiloSessionId: KILO_SESSION_ID, + [SESSION_CREATE_WORKTREE_ENABLED_KEY]: false, + [SESSION_CREATE_FINALIZATION_VERSION_KEY]: 2, + createIntentFingerprint: await sessionCreateIntentFingerprint(request), + }); + expect(getControlSession).toHaveBeenCalledOnce(); + expect(doStub.registerSessionFromMetadata).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + identity: expect.objectContaining({ sessionId: CONTROL_CLOUD_AGENT_SESSION_ID }), + workspace: expect.objectContaining({ + sandboxId: PLACED_SANDBOX_ID, + sandboxProvider: 'vercel', + sandboxProviderBinding: { + kind: 'vercel', + source: { kind: 'byoc', organizationId, credentialId }, + }, + }), + }), + }) + ); + expect(result).toEqual({ + cloudAgentSessionId: CONTROL_CLOUD_AGENT_SESSION_ID, + kiloSessionId: KILO_SESSION_ID, + }); + }); + it('surfaces BAD_REQUEST session_clone_failed when the clone is rejected', async () => { createCliSessionMock.mockResolvedValue({ status: 'rejected', code: 'source_access_denied' }); const doStub = makeDoStub(); @@ -5353,6 +5931,7 @@ describe('createSessionWithLedger clone allocation outcomes', () => { expect(recordOperationProgressMock).toHaveBeenNthCalledWith(2, expect.any(Object), ROW_ID, { sandboxId: expect.stringMatching(/^sbx-/), sandboxProvider: PLACED_SANDBOX_DECISION.provider, + sandboxProviderBinding: { kind: PLACED_SANDBOX_DECISION.provider }, sandboxKind: PLACED_SANDBOX_DECISION.sandboxKind, }); }); @@ -5445,6 +6024,7 @@ describe('createSessionWithLedger clone reconciliation', () => { beforeEach(async () => { vi.clearAllMocks(); + fetchByocVercelEnrollmentMock.mockReset(); const actualPlacement = await vi.importActual('../sandbox-placement.js'); selectControlPlaneSandboxMock.mockImplementation(actualPlacement.selectControlPlaneSandbox); @@ -5603,7 +6183,22 @@ describe('createSessionWithLedger clone reconciliation', () => { const request = cloneRequest(); admitOperationMock.mockResolvedValueOnce({ admission: 'takeover', - row: await cloneRow({ cloudAgentSessionId: sessionId, sandboxProvider }), + row: await cloneRow({ + cloudAgentSessionId: sessionId, + sandboxProvider, + ...(sandboxProvider === 'vercel' + ? { + sandboxProviderBinding: { + kind: 'vercel', + source: { + kind: 'byoc', + userId: USER_ID, + credentialId: '22222222-2222-4222-8222-222222222222', + }, + }, + } + : {}), + }), }); createCliSessionMock.mockResolvedValue({ status: 'ready', diff --git a/services/cloud-agent-next/src/session/session-registration-metadata.ts b/services/cloud-agent-next/src/session/session-registration-metadata.ts index a9236b1b860..97bee7f41af 100644 --- a/services/cloud-agent-next/src/session/session-registration-metadata.ts +++ b/services/cloud-agent-next/src/session/session-registration-metadata.ts @@ -36,6 +36,7 @@ export type GroupedRegisterSessionInput = { | 'sandboxRoute' | 'sandboxKind' | 'sandboxProvider' + | 'sandboxProviderBinding' | 'shallow' | 'credentialContainment' | 'devcontainerRequested' diff --git a/services/cloud-agent-next/src/session/session-registration.ts b/services/cloud-agent-next/src/session/session-registration.ts index abac04dfa7e..efc709e89e3 100644 --- a/services/cloud-agent-next/src/session/session-registration.ts +++ b/services/cloud-agent-next/src/session/session-registration.ts @@ -1,3 +1,4 @@ +import { enrolledByocVercelOwner, byocVercelOwnerFields } from '../byoc/vercel-owner.js'; /** * Session creation helpers for the grouped `start` path and retained split * `prepareSession` path. @@ -27,6 +28,9 @@ import { import { assertSandboxAllocationAvailable, getSandboxSelectionCapabilities, + isByocIncompatibleAllocation, + sandboxAllocationRepositoryUnavailableReason, + sandboxAllocationProviderCapabilityReason, isSandboxAllocationAvailable, } from '../sandbox-selection.js'; import { assertOrganizationMembership } from '../router/handlers/organization-membership.js'; @@ -61,6 +65,16 @@ import { type CredentialContainment, type SessionMetadata, } from '../persistence/session-metadata.js'; +import { + bindingFromLegacyProvider, + SandboxProviderBindingSchema, + type SandboxProviderBinding, +} from '../sandbox-provider-binding.js'; +import { + fetchByocVercelEnrollment, + fetchByocVercelEnrollmentForUser, + ByocCredentialMissingError, +} from '../byoc/vercel-credential-resolver.js'; import { logger } from '../logger.js'; import { withDORetry } from '../utils/do-retry.js'; import { getSandboxSessionStub, type SessionStub } from '../sandbox-session/session-stub.js'; @@ -117,6 +131,7 @@ export type SessionRegistrationInput = SessionCreateRequest; function assertSupportedSandboxAllocation( input: SessionRegistrationInput, + ctx: SessionRegistrationContext, options?: { billingOrigin?: string } ): void { if (!egressPolicySchema.safeParse(input.runtime ?? {}).success) { @@ -125,11 +140,41 @@ function assertSupportedSandboxAllocation( if (input.runtime && 'devcontainer' in input.runtime && input.runtime.devcontainer === true) { throw new TRPCError({ code: 'BAD_REQUEST', message: DEVCONTAINER_RETIRED_MESSAGE }); } + if ( + options?.billingOrigin !== 'code-review' && + enrolledByocVercelOwner(ctx.env, { + userId: ctx.userId, + orgId: input.options?.kilocodeOrganizationId, + }) !== undefined + ) { + const unavailableReason = + sandboxAllocationRepositoryUnavailableReason('vercel-small', input.repository.type) ?? + sandboxAllocationProviderCapabilityReason( + 'vercel-small', + allocationCapabilityFor(input, options?.billingOrigin) + ); + if (unavailableReason) { + throw new TRPCError({ code: 'BAD_REQUEST', message: unavailableReason }); + } + } const allocation = input.runtime?.sandboxAllocation; if (allocation === undefined) return; if (!sandboxAllocationSchema.safeParse(allocation).success) { throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid sandbox allocation' }); } + if ( + options?.billingOrigin !== 'code-review' && + isByocIncompatibleAllocation( + ctx.env, + { userId: ctx.userId, orgId: input.options?.kilocodeOrganizationId }, + allocation + ) + ) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Sandbox allocation is not supported for BYOC Vercel accounts', + }); + } if ( options?.billingOrigin === 'code-review' || input.options?.createdOnPlatform === 'code-review' @@ -174,6 +219,7 @@ export type SessionRegistrationResult = { sandboxProvider: SandboxSelection['provider']; worktreeId?: CloudAgentWorktreeId; sandboxAllocation?: SandboxAllocation; + sandboxProviderBinding: SandboxProviderBinding; /** * Canonical initial turn reserved for a later split initiation request. * Omitted for a clone-only create, which has no synthetic initial turn. @@ -717,19 +763,26 @@ async function allocateNewSession( ); } const initialTurn = input.initialTurn ? acceptInitialTurn(input.initialTurn) : undefined; + const isCodeReviewSession = options?.billingOrigin === 'code-review'; + const byocOwner = isCodeReviewSession + ? undefined + : enrolledByocVercelOwner(ctx.env, { userId: ctx.userId, orgId }); + const byocEnrolled = byocOwner !== undefined; const cloudAgentSessionId = generateSessionId(); - const sandboxAllocation = resolveNewSessionSandboxAllocation({ - env: ctx.env, - orgId, - userId: ctx.userId, - sessionId: cloudAgentSessionId, - requestedAllocation, - contained: hasCredentialContainment( - computeCredentialContainment(cloudAgentSessionId, input, ctx.env) - ), - createdOnPlatform: input.options?.createdOnPlatform, - billingOrigin: options?.billingOrigin, - }); + const sandboxAllocation = byocEnrolled + ? requestedAllocation + : resolveNewSessionSandboxAllocation({ + env: ctx.env, + orgId, + userId: ctx.userId, + sessionId: cloudAgentSessionId, + requestedAllocation, + contained: hasCredentialContainment( + computeCredentialContainment(cloudAgentSessionId, input, ctx.env) + ), + createdOnPlatform: input.options?.createdOnPlatform, + billingOrigin: options?.billingOrigin, + }); assertCloudAgentIsolatedAllocation( input.allowSubagentSessions === true || input.parentCloudAgentSessionId !== undefined, sandboxAllocation @@ -786,7 +839,25 @@ async function allocateNewSession( let sandboxId: SandboxId; let sandboxKind: SandboxKind | undefined; let sandboxProvider: SandboxSelection['provider'] = 'cloudflare'; + let sandboxProviderBinding: SandboxProviderBinding = { kind: 'cloudflare' }; + let byocEnrollment: Awaited> | undefined; try { + if (byocOwner) { + try { + byocEnrollment = + byocOwner.organizationId !== undefined + ? await fetchByocVercelEnrollment(ctx.env, byocOwner.organizationId) + : await fetchByocVercelEnrollmentForUser(ctx.env, byocOwner.userId); + } catch (error) { + if (error instanceof ByocCredentialMissingError) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'byoc_vercel_not_ready' }); + } + throw error; + } + if (byocEnrollment.setupStatus !== 'ready') { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'byoc_vercel_not_ready' }); + } + } const decision = await selectControlPlaneSandbox({ env: ctx.env, sessionId: cloudAgentSessionId, @@ -799,6 +870,19 @@ async function allocateNewSession( sandboxId = decision.sandboxId; sandboxKind = decision.sandboxKind; sandboxProvider = decision.provider; + if (byocEnrollment) { + sandboxProvider = 'vercel'; + sandboxProviderBinding = { + kind: 'vercel', + source: { + kind: 'byoc', + ...byocVercelOwnerFields(byocEnrollment), + credentialId: byocEnrollment.credentialId, + }, + }; + } else { + sandboxProviderBinding = bindingFromLegacyProvider(sandboxProvider); + } } catch (error) { await recordPostSetupFailure(() => recordCloudAgentSessionFailure( @@ -833,6 +917,7 @@ async function allocateNewSession( await recordOperationProgress(ledger.db, ledger.rowId, { sandboxId, sandboxProvider, + sandboxProviderBinding, ...(sandboxKind ? { sandboxKind } : {}), }); } catch (error) { @@ -945,6 +1030,7 @@ async function allocateNewSession( sandboxProvider, ...(worktreeId ? { worktreeId } : {}), ...(sandboxAllocation ? { sandboxAllocation } : {}), + sandboxProviderBinding, initialTurn, credentialContainment, rollbackCliSession: async () => { @@ -990,6 +1076,7 @@ function rebuildRecordedSessionAllocation( const initialMessageId = canonical.initialMessageId; const sandboxId = canonical.sandboxId; const sandboxProvider = canonical.sandboxProvider; + const sandboxProviderBinding = canonical.sandboxProviderBinding; // A legacy prefixed key recorded no kind; a placed key never rebuilds without one. const recordedKind = sandboxKindSchema.optional().safeParse(canonical.sandboxKind); if ( @@ -1065,6 +1152,16 @@ function rebuildRecordedSessionAllocation( assertEgressPolicyAllocation(input, { sandboxProvider: recordedProvider.data, sandboxKind }); const cloneFromKiloSessionId = input.clone?.cloneFromKiloSessionId; + const parsedBinding = SandboxProviderBindingSchema.safeParse( + sandboxProviderBinding === undefined + ? bindingFromLegacyProvider(recordedProvider.data) + : sandboxProviderBinding + ); + if (!parsedBinding.success || parsedBinding.data.kind !== recordedProvider.data) { + throw creationInProgressError(); + } + const normalizedBinding = parsedBinding.data; + return { cloudAgentSessionId, kiloSessionId, @@ -1073,6 +1170,7 @@ function rebuildRecordedSessionAllocation( sandboxProvider: recordedProvider.data, ...(worktreeId ? { worktreeId } : {}), ...(recorded.data ? { sandboxAllocation: recorded.data } : {}), + sandboxProviderBinding: normalizedBinding, initialTurn, credentialContainment: computeCredentialContainment(cloudAgentSessionId, input, ctx.env), rollbackCliSession: async () => { @@ -1160,6 +1258,7 @@ function buildSessionRegistrationCommand( ? { egressAllowlist: egressAllowlistSchema.parse(input.runtime.egressAllowlist ?? []) } : {}), ...(allocation.sandboxAllocation ? { sandboxAllocation: allocation.sandboxAllocation } : {}), + sandboxProviderBinding: allocation.sandboxProviderBinding, shallow: input.options?.shallow, ...(allocation.worktreeId ? { @@ -1222,7 +1321,7 @@ export async function registerNewSession( ctx: SessionRegistrationContext, options?: { billingOrigin?: string } ): Promise { - assertSupportedSandboxAllocation(rawInput, options); + assertSupportedSandboxAllocation(rawInput, ctx, options); const input = rawInput; const allocation = await allocateNewSession(input, ctx, options); let registerResult: { success: boolean; error?: string }; @@ -1391,6 +1490,7 @@ async function registerAndAdmitInitialTurn( sandboxId: allocation.sandboxId, sandboxProvider: allocation.sandboxProvider, ...(allocation.sandboxAllocation ? { sandboxAllocation: allocation.sandboxAllocation } : {}), + sandboxProviderBinding: allocation.sandboxProviderBinding, admission, }; if (ledger) { @@ -1513,7 +1613,7 @@ export async function startNewSession( options?: { billingOrigin?: string }, ledger?: SessionCreationLedgerHooks ): Promise { - assertSupportedSandboxAllocation(rawInput, options); + assertSupportedSandboxAllocation(rawInput, ctx, options); // A ledger create has resolved the saved allocation already; this is a no-op then. const input = await withSavedSandboxAllocation(rawInput, savedAllocationArgs(ctx, options)); const allocation = await allocateSessionForCreate(input, ctx, options, ledger); @@ -1806,7 +1906,7 @@ export async function createSessionWithLedger( ctx: SessionRegistrationContext, options: SessionLedgerCreateOptions ): Promise { - assertSupportedSandboxAllocation(rawInput, { billingOrigin: options.billingOrigin }); + assertSupportedSandboxAllocation(rawInput, ctx, { billingOrigin: options.billingOrigin }); await assertSandboxAllocationMembership(rawInput, ctx); const db = getPgDb(ctx.env); const input = options.resolveSavedAllocationBeforeAdmission diff --git a/services/cloud-agent-next/src/shared/assistant-failure.ts b/services/cloud-agent-next/src/shared/assistant-failure.ts index a647c71a1d2..3cd4696a47b 100644 --- a/services/cloud-agent-next/src/shared/assistant-failure.ts +++ b/services/cloud-agent-next/src/shared/assistant-failure.ts @@ -1,3 +1,4 @@ +import type { KiloSdkAssistantError } from '@kilocode/session-ingest-contracts'; import { z } from 'zod'; import type { CloudAgentAssistantFailureReason, @@ -494,3 +495,52 @@ export function assistantReportsNoActionableOutput(parts: readonly unknown[]): b const text = texts.join('\n'); return /no actionable output/i.test(text) && /(?:output|token) limit/i.test(text); } + +export function projectSafeSdkAssistantError(source: unknown): KiloSdkAssistantError | undefined { + const message = projectSafeAssistantError(source); + if (message === undefined) return undefined; + if (isAssistantInterrupt(source)) return { name: 'MessageAbortedError', data: { message } }; + if (typeof source === 'object' && source !== null && !Array.isArray(source) && 'name' in source) { + const data = + 'data' in source && + typeof source.data === 'object' && + source.data !== null && + !Array.isArray(source.data) + ? source.data + : {}; + switch (source.name) { + case 'ProviderAuthError': + return { name: source.name, data: { providerID: 'unknown', message } }; + case 'MessageOutputLengthError': + case 'ContextOverflowError': + return { name: source.name, data: { message } }; + case 'APIError': { + if (!('isRetryable' in data) || typeof data.isRetryable !== 'boolean') break; + const statusCode = 'statusCode' in data ? data.statusCode : undefined; + return { + name: source.name, + data: { + message, + isRetryable: data.isRetryable, + ...(typeof statusCode === 'number' && + Number.isInteger(statusCode) && + statusCode >= 100 && + statusCode <= 599 + ? { statusCode } + : {}), + }, + }; + } + case 'StructuredOutputError': + if ( + 'retries' in data && + typeof data.retries === 'number' && + Number.isSafeInteger(data.retries) && + data.retries >= 0 + ) { + return { name: source.name, data: { message, retries: data.retries } }; + } + } + } + return { name: 'UnknownError', data: { message } }; +} diff --git a/services/cloud-agent-next/src/shared/control-plane-protocol.ts b/services/cloud-agent-next/src/shared/control-plane-protocol.ts index 21db190e212..30098e413c2 100644 --- a/services/cloud-agent-next/src/shared/control-plane-protocol.ts +++ b/services/cloud-agent-next/src/shared/control-plane-protocol.ts @@ -124,6 +124,11 @@ export const CONTROL_PLANE_FAILURE_REASON_VALUES = [ 'prompt_failed', 'sandbox_stopped', 'execution_limit', + 'byoc_credential_missing', + 'byoc_vercel_not_ready', + 'byoc_vercel_forbidden', + 'byoc_vercel_capacity', + 'byoc_vercel_snapshot_missing', ] as const; export type ControlPlaneFailureReason = (typeof CONTROL_PLANE_FAILURE_REASON_VALUES)[number]; diff --git a/services/cloud-agent-next/src/shared/wrapper-version.ts b/services/cloud-agent-next/src/shared/wrapper-version.ts index 533a0834678..4c917b610c2 100644 --- a/services/cloud-agent-next/src/shared/wrapper-version.ts +++ b/services/cloud-agent-next/src/shared/wrapper-version.ts @@ -1 +1,2 @@ export const WRAPPER_VERSION = '2.4.0'; +export const WRAPPER_RELEASED_AT = '2026-09-08'; diff --git a/services/cloud-agent-next/src/types.ts b/services/cloud-agent-next/src/types.ts index cfaa915b36f..de825a8f02d 100644 --- a/services/cloud-agent-next/src/types.ts +++ b/services/cloud-agent-next/src/types.ts @@ -10,6 +10,7 @@ import type { AccessibleCloudAgentSession } from '@kilocode/worker-utils/cloud-a import type { UserKiloFacade } from './kilo-facade/user-kilo-facade.js'; import type { SandboxControlV2 as SandboxControl } from './control-plane/sandbox/sandbox-do.js'; import type { SandboxSessionV2 as SandboxSession } from './control-plane/session/session-do.js'; +import type { VercelSnapshotBuild } from './persistence/VercelSnapshotBuild.js'; import type { SandboxContainers } from './sandbox-containers/SandboxContainers.js'; import type { StreamTicketNonceDO } from './persistence/StreamTicketNonceDO.js'; // Type-only exception to "no reverse import from production code": erased at @@ -538,6 +539,8 @@ export type Env = { SandboxCodeReviewContainment: DurableObjectNamespace; /** Durable Object namespace for sandbox-scoped wrapper call-home control */ SANDBOX_CONTROL: DurableObjectNamespace; + /** Durable Object namespace for organization-scoped BYOC runtime snapshot builds. */ + VERCEL_SNAPSHOT_BUILD?: DurableObjectNamespace; /** Durable Object namespace for control-plane sessions */ SANDBOX_SESSION: DurableObjectNamespace; /** Durable Object namespace for DO-managed Cloudflare containers (control-plane only) */ @@ -619,6 +622,8 @@ export type Env = { CLOUD_AGENT_CONTAINER_CONCURRENCY_DISABLED?: string; /** Comma-separated user or org IDs whose Code Reviewer sessions use the Cloudflare Containers runtime. `*` includes personal. */ CODE_REVIEW_CONTAINERS_ORG_IDS?: string; + /** User or organization IDs enrolled in customer-paid Vercel compute. */ + BYOC_VERCEL_IDS?: string; RUNTIME_ISOLATION_ENABLED?: string; /** Comma-separated user or org IDs allowed to pick a sandbox destination. `*` includes personal. */ SANDBOX_SELECTION_IDS?: string; @@ -635,16 +640,9 @@ export type Env = { TOOL_CGROUP_OOM_GROUP?: string; TOOL_CGROUP_CPU_WEIGHT?: string; TOOL_CGROUP_SERVER_CPU_WEIGHT?: string; - VERCEL_TOKEN?: string; - VERCEL_TEAM_ID?: string; - VERCEL_PROJECT_ID?: string; - VERCEL_SANDBOX_SNAPSHOT_ID?: string; - VERCEL_SANDBOX_RUNTIME_BUILD_ID?: string; VERCEL_SANDBOX_RUNTIME?: string; VERCEL_SANDBOX_INITIAL_TIMEOUT_MS?: string; VERCEL_SANDBOX_EXTEND_DURATION_MS?: string; - /** Comma-separated org IDs routed to Vercel. Empty is off. `*` includes personal. */ - VERCEL_SANDBOX_ORG_IDS?: string; /** Comma-separated org IDs routed to DO-managed Cloudflare containers. Empty is off. `*` includes personal. */ CLOUDFLARE_CONTAINERS_ORG_IDS?: string; /** R2 endpoint for S3-compatible API access (presigned URL generation) */ diff --git a/services/cloud-agent-next/test/integration/byoc-control-plane-v2.test.ts b/services/cloud-agent-next/test/integration/byoc-control-plane-v2.test.ts new file mode 100644 index 00000000000..e9a47da15ea --- /dev/null +++ b/services/cloud-agent-next/test/integration/byoc-control-plane-v2.test.ts @@ -0,0 +1,139 @@ +import { env, reset, runInDurableObject } from 'cloudflare:test'; +import { eq } from 'drizzle-orm'; +import { drizzle } from 'drizzle-orm/durable-sqlite'; +import { allocation } from '../../src/control-plane/sandbox/sqlite-schema.js'; +import { afterEach, describe, expect, it } from 'vitest'; +import type { SandboxControlV2 } from '../../src/control-plane/sandbox/sandbox-do.js'; +import { resolveSecret } from '../../src/auth.js'; +import { + generateSandboxCredential, + hashSandboxCredential, + mintSandboxLaunchCredential, +} from '../../src/sandbox-control/credential.js'; +import { encodeVercelProviderRef } from '../../src/sandbox-control/vercel-provider.js'; +import { CONTROL_PLANE_PROTOCOL_VERSION } from '../../src/shared/control-plane-protocol.js'; +import { FakeWrapper } from './helpers/fake-wrapper.js'; +import { waitFor } from './wait-for.js'; + +const sandboxes = (env as unknown as { SANDBOX_CONTROL: DurableObjectNamespace }) + .SANDBOX_CONTROL; + +afterEach(async () => { + await reset(); +}); + +describe('Vercel requires customer credentials', () => { + it('rejects a Kilo-paid allocation before any provider operation', async () => { + const stub = sandboxes.getByName('ses-retired-vercel'); + const rejection = await runInDurableObject(stub, async instance => { + try { + await instance.ensureAllocation({ provider: 'vercel' }); + return null; + } catch (error) { + return error instanceof Error ? error.message : 'unknown'; + } + }); + expect(rejection).toBe('byoc_credential_missing'); + expect((await stub.getAllocationState()).kind).toBe('stopped'); + }); +}); + +describe('BYOC snapshot validator on the current control plane', () => { + it.each(['organization', 'personal'] as const)( + 'persists the %s customer identity, accepts the signed wrapper, and confirms cleanup', + async ownerType => { + const generation = crypto.randomUUID(); + const sandboxId = `ses-byoc-validator-${generation.replaceAll('-', '')}`; + const allocationId = generation; + const credential = generateSandboxCredential(); + const providerRef = encodeVercelProviderRef({ + sandboxName: `ses-validator-${generation}`, + sessionId: 'sbox_validator', + }); + const stub = sandboxes.getByName(sandboxId); + const input = { + build: { + generation, + credentialId: crypto.randomUUID(), + ...(ownerType === 'organization' + ? { organizationId: crypto.randomUUID() } + : { userId: 'oauth/google/personal-owner' }), + }, + allocation: { + providerRef, + locator: { + teamId: 'team_customer', + projectId: 'prj_customer', + snapshotId: 'snap_customer', + runtimeBuildId: 'build_customer', + runtime: 'node24' as const, + }, + createdAt: Date.now(), + expiresAt: Date.now() + 600_000, + }, + credentialHash: await hashSandboxCredential(credential), + }; + await stub.initializeSnapshotValidator(input); + await stub.initializeSnapshotValidator(input); + const pin = await runInDurableObject( + stub, + (_instance, state) => + drizzle(state.storage) + .select({ pin: allocation.provider_pin }) + .from(allocation) + .where(eq(allocation.id, 'current')) + .get()?.pin + ); + // The persisted provider identity contains references only. + expect(JSON.stringify(pin)).toContain(input.build.credentialId); + expect(JSON.parse(pin ?? 'null')).toMatchObject({ + byoc: + ownerType === 'organization' + ? { organizationId: input.build.organizationId } + : { userId: input.build.userId }, + billing: null, + }); + expect(JSON.stringify(pin)).not.toContain(credential); + expect(JSON.stringify(pin)).not.toContain('accessToken'); + const secret = await resolveSecret(env.NEXTAUTH_SECRET); + if (!secret) throw new Error('Test signing secret is unavailable'); + const wrapper = await FakeWrapper.connect({ + sandboxId, + credential: mintSandboxLaunchCredential({ sandboxId, allocationId, credential }, secret), + }); + wrapper.send({ + type: 'hello', + protocolVersion: CONTROL_PLANE_PROTOCOL_VERSION, + allocationId, + wrapperId: 'byoc-validator-wrapper', + }); + expect(await wrapper.next()).toMatchObject({ type: 'welcome' }); + await waitFor(async () => (await stub.getAllocationState()).kind === 'connected'); + expect(await stub.getSnapshotValidatorStatus()).toEqual({ + physical: 'running', + connection: 'ready', + }); + const mismatchedCleanup = await runInDurableObject(stub, async instance => { + try { + await instance.confirmSnapshotValidatorStopped('different-ref'); + return null; + } catch (error) { + return error instanceof Error ? error.message : 'unknown'; + } + }); + expect(mismatchedCleanup).toContain('allocation changed'); + await stub.confirmSnapshotValidatorStopped(providerRef); + expect((await stub.getAllocationState()).kind).toBe('stopped'); + const restart = await runInDurableObject(stub, async instance => { + try { + await instance.initializeSnapshotValidator(input); + return null; + } catch (error) { + return error instanceof Error ? error.message : 'unknown'; + } + }); + expect(restart).toContain('allocation changed'); + wrapper.close(); + } + ); +}); diff --git a/services/cloud-agent-next/test/integration/control-plane-scope-handle.test.ts b/services/cloud-agent-next/test/integration/control-plane-scope-handle.test.ts index ae271cad739..4d49bfb658b 100644 --- a/services/cloud-agent-next/test/integration/control-plane-scope-handle.test.ts +++ b/services/cloud-agent-next/test/integration/control-plane-scope-handle.test.ts @@ -44,7 +44,7 @@ const sessions = (env as unknown as { SANDBOX_SESSION: SessionNamespace }).SANDB const USER_ID = 'user_123'; const OTHER_USER_ID = 'user_other'; -const ORG_ID = 'org_123'; +const ORG_ID = '11111111-1111-4111-8111-111111111111'; const WORKER_URL = 'https://control.test'; const SECRET = env.NEXTAUTH_SECRET as string; @@ -213,7 +213,10 @@ async function createSession(input: { turn: { type: 'prompt', prompt: 'hello' }, agent: { mode: 'code', model: 'test/model' }, }, - sandboxSelection: { provider: 'vercel' }, + sandboxSelection: { + provider: 'vercel', + byoc: { organizationId: ORG_ID, credentialId: '22222222-2222-4222-8222-222222222222' }, + }, ...(input.seal ? { runtimeAuthorizationSeal: input.seal } : {}), }); return sessionStub; diff --git a/services/cloud-agent-next/test/integration/control-plane-session-v2-rpc.test.ts b/services/cloud-agent-next/test/integration/control-plane-session-v2-rpc.test.ts index c2c13bed4c8..733a57054fb 100644 --- a/services/cloud-agent-next/test/integration/control-plane-session-v2-rpc.test.ts +++ b/services/cloud-agent-next/test/integration/control-plane-session-v2-rpc.test.ts @@ -36,7 +36,7 @@ const sandboxes = (env as unknown as { SANDBOX_CONTROL: SandboxControlNamespace const sessions = (env as unknown as { SANDBOX_SESSION: SessionNamespace }).SANDBOX_SESSION; const USER_ID = 'user_123'; -const ORG_ID = 'org_123'; +const ORG_ID = '11111111-1111-4111-8111-111111111111'; let sequence = 0; function b64url(value: string): string { @@ -498,7 +498,11 @@ describe('SandboxSessionV2 worker RPC surface', () => { }, }), message: promptPayload(firstMessageId), - sandboxSelection: { provider: 'vercel', allocationName: 'alloc_test' }, + sandboxSelection: { + provider: 'vercel', + allocationName: 'alloc_test', + byoc: { organizationId: ORG_ID, credentialId: '22222222-2222-4222-8222-222222222222' }, + }, }); // M2: legacy admission ack shape. @@ -528,10 +532,12 @@ describe('SandboxSessionV2 worker RPC surface', () => { messages: [{ messageId: firstMessageId, state: 'queued' }], }); - // H1: the non-default provider/allocation reached the sandbox pin. + // Vercel pins each physical allocation under its own immutable name. + const allocation = await sandboxStub.getAllocationState(); + expect(allocation.allocationId).toEqual(expect.any(String)); await expect(sandboxStub.getProviderRuntime()).resolves.toEqual({ provider: 'vercel', - allocationName: 'alloc_test', + allocationName: `sbx-${allocation.allocationId?.replaceAll('-', '')}`, }); expect(provider.createCalls).toBe(1); @@ -726,7 +732,10 @@ describe('SandboxSessionV2 worker RPC surface', () => { sessionStub.createSessionWithInitialAdmission({ metadata: metadata({ sessionId, kiloSessionId: kiloSessionId(), sandboxId }), message: promptPayload(messageId()), - sandboxSelection: { provider: 'vercel' }, + sandboxSelection: { + provider: 'vercel', + byoc: { organizationId: ORG_ID, credentialId: '22222222-2222-4222-8222-222222222222' }, + }, }) ).resolves.toEqual({ success: false, diff --git a/services/cloud-agent-next/test/integration/sandbox-control-v2-credentials.test.ts b/services/cloud-agent-next/test/integration/sandbox-control-v2-credentials.test.ts index 98ca0009849..fbf9afb9e89 100644 --- a/services/cloud-agent-next/test/integration/sandbox-control-v2-credentials.test.ts +++ b/services/cloud-agent-next/test/integration/sandbox-control-v2-credentials.test.ts @@ -374,6 +374,10 @@ async function setupVercel( await runInDurableObject(stub, async instance => { await instance.ensureAllocation({ provider: 'vercel', + byoc: { + userId: 'user_123', + credentialId: '22222222-2222-4222-8222-222222222222', + }, allocationName: SANDBOX_ID, containment: WORKTREE_CREDENTIAL_CONTAINMENT, }); diff --git a/services/cloud-agent-next/test/integration/sandbox-control-v2-runtime-proxy.test.ts b/services/cloud-agent-next/test/integration/sandbox-control-v2-runtime-proxy.test.ts index d1f52788233..65f48a0b4b7 100644 --- a/services/cloud-agent-next/test/integration/sandbox-control-v2-runtime-proxy.test.ts +++ b/services/cloud-agent-next/test/integration/sandbox-control-v2-runtime-proxy.test.ts @@ -50,7 +50,7 @@ const sandboxes = (env as unknown as { SANDBOX_CONTROL: SandboxControlNamespace const sessions = (env as unknown as { SANDBOX_SESSION: SessionNamespace }).SANDBOX_SESSION; const USER_ID = 'user_123'; -const ORG_ID = 'org_123'; +const ORG_ID = '11111111-1111-4111-8111-111111111111'; const WORKER_URL = 'https://control.test'; const SECRET = env.NEXTAUTH_SECRET as string; const FACADE_BASE = runtimeCredentialProxyFacadeBaseUrl(WORKER_URL); @@ -259,6 +259,14 @@ async function createVercelSession(input: { message: promptPayload(messageId()), sandboxSelection: { provider: input.provider ?? 'vercel', + ...((input.provider ?? 'vercel') === 'vercel' + ? { + byoc: { + organizationId: ORG_ID, + credentialId: '22222222-2222-4222-8222-222222222222', + }, + } + : {}), ...(input.contained === undefined ? {} : { diff --git a/services/cloud-agent-next/test/integration/sandbox-control-v2.test.ts b/services/cloud-agent-next/test/integration/sandbox-control-v2.test.ts index b4bc950abbd..3df08fa144f 100644 --- a/services/cloud-agent-next/test/integration/sandbox-control-v2.test.ts +++ b/services/cloud-agent-next/test/integration/sandbox-control-v2.test.ts @@ -18,12 +18,7 @@ import type { EgressMode } from '@kilocode/worker-utils/egress-allowlist'; import { eq } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/durable-sqlite'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { - clearBillingContext, - getBillingContext, - updateBillingContext, - type ContainerUsageRpcMethods, -} from '@kilocode/container-usage'; +import { type ContainerUsageRpcMethods } from '@kilocode/container-usage'; import type { SandboxControlV2, StoredProviderPin, @@ -32,12 +27,6 @@ import { allocation as allocationTable, routes as routesTable, } from '../../src/control-plane/sandbox/sqlite-schema.js'; -import { listScopeGrants, writeScopeGrant } from '../../src/control-plane/sandbox/scope-grants.js'; -import { createControlPlaneCredential } from '../../src/sandbox-control/managed-credential.js'; -import { - buildControlNetworkPolicy, - type SessionCredentialGrant, -} from '../../src/sandbox-control/session-credentials.js'; import type { ProviderAdapter, ProviderCreateIntent, @@ -45,12 +34,8 @@ import type { StopResult, } from '../../src/sandbox-control/provider.js'; import { ProviderCreationError } from '../../src/sandbox-control/provider.js'; -import { - ContainerCapacityUnavailableError, - ContainerConcurrencyLimitError, -} from '../../src/container-concurrency.js'; -import { VERCEL_BILLING_SETTLEMENT_CALLBACK } from '../../src/sandbox-control/vercel-billing.js'; -import { encodeVercelProviderRef } from '../../src/sandbox-control/vercel-provider.js'; +import { ContainerConcurrencyLimitError } from '../../src/container-concurrency.js'; +import { BILLING_HEARTBEAT_CALLBACK as VERCEL_BILLING_SETTLEMENT_CALLBACK } from '../../src/metered-billing-lifecycle.js'; import { encodeCloudflareProviderRef } from '../../src/sandbox-control/cloudflare-provider.js'; import type { ContainersLaunchInput } from '../../src/sandbox-containers/SandboxContainers.js'; import { @@ -80,49 +65,13 @@ type SandboxControlNamespace = DurableObjectNamespace; const sandboxNamespace = (env as unknown as { SANDBOX_CONTROL: SandboxControlNamespace }) .SANDBOX_CONTROL; -const GRANT_TARGET = 'https://worker.example.com'; -/** Kilo token that decodes to `runtimeAuthorization`, enabling the runtime proxy. */ +/** Test token with a runtime authorization claim, enabling the runtime proxy. */ const RUNTIME_AUTHORIZED_KILO_TOKEN = 'eyJhbGciOiJub25lIn0.eyJydW50aW1lQXV0aG9yaXphdGlvbiI6eyJpZCI6InJhXzEifX0.c2ln'; -const GRANT_MEMBER = { - sessionId: 'workspace_11111111-1111-1111-1111-111111111111', - kiloSessionId: 'ses_aaaaaaaaaaaaaaaaaaaaaaaaaa', -}; - -/** A schema-valid contained Vercel grant carrying a bound runtime-proxy handle. */ -function containedGrant( - sandboxId: string, - options: { scopeId: string; handle: string; preparedAt: number; expiresAt: number } -): SessionCredentialGrant { - const targets = { - backendBaseUrl: GRANT_TARGET, - providerBaseUrl: GRANT_TARGET, - sessionIngestBaseUrl: GRANT_TARGET, - }; - return { - version: 1, - containmentEnabled: true, - scopeId: options.scopeId, - sandboxId, - directory: `/workspace/${options.scopeId}`, - userId: 'owner-1', - provider: 'vercel', - outboundContainerId: 'outbound-1', - members: [{ ...GRANT_MEMBER }], - kilo: { - alias: createControlPlaneCredential(sandboxId, 'kilo'), - token: RUNTIME_AUTHORIZED_KILO_TOKEN, - targets, - runtimeProxy: { targets, members: [{ ...GRANT_MEMBER, handle: options.handle }] }, - capabilities: {}, - }, - preparedAt: options.preparedAt, - expiresAt: options.expiresAt, - }; -} type FakeProviderOptions = { failFirstCreate?: boolean; + failFirstLaunch?: string; gateCreate?: boolean; gateLaunch?: boolean; gateStop?: boolean; @@ -175,6 +124,9 @@ function createFakeProvider(options: FakeProviderOptions = {}): FakeProvider { }, async launch(_ref, { env }) { provider.launchEnvs.push({ ...env }); + if (options.failFirstLaunch !== undefined && provider.launchEnvs.length === 1) { + throw new Error(options.failFirstLaunch); + } if (options.gateLaunch) { await new Promise((resolve, reject) => { provider.launchGates.push(error => (error === undefined ? resolve() : reject(error))); @@ -213,10 +165,11 @@ type StartOptions = { configuration?: SandboxProviderConfiguration; meter?: ContainerUsageRpcMethods; preparingRoute?: string; - admissionError?: string; egressMode?: EgressMode; egressAllowlist?: string[]; sandboxKind?: StoredProviderPin['sandboxKind']; + /** Shrink sandbox timers (e.g. `providerCreateMs`) for expiry tests. */ + timers?: Partial; }; /** Injects the fake adapter, then triggers `ensure` through a real DO RPC so @@ -230,34 +183,20 @@ async function startAllocation( await runInDurableObject(stub, async (instance, state) => { await instance.getAllocationState(); if (options.meter !== undefined) await state.storage.put('control_plane_owner', 'owner-1'); - const realAdmission = ( - instance as unknown as { admitVercelCreate(pin: unknown): Promise } - ).admitVercelCreate.bind(instance); Object.assign(instance, { createProviderAdapter: () => provider.adapter, provider: provider.adapter, ...(options.meter === undefined ? {} - : { env: { ...instance.env, CONTAINER_USAGE_METER: options.meter } }), - ...(options.admissionError === undefined + : { + env: { + ...instance.env, + ...(options.meter === undefined ? {} : { CONTAINER_USAGE_METER: options.meter }), + }, + }), + ...(options.timers === undefined ? {} - : options.admissionError === 'hang' - ? { - admitVercelCreate: () => new Promise(() => undefined), - sandboxTimers: () => ({ ...TIMERS, providerStopAttemptMs: 20 }), - } - : options.admissionError === 'throw-after-open' - ? { - admitVercelCreate: async (pin: unknown) => { - await realAdmission(pin); - throw new Error('Meter response lost after context write'); - }, - } - : { - admitVercelCreate: async () => { - throw new Error(options.admissionError); - }, - }), + : { sandboxTimers: () => ({ ...TIMERS, ...options.timers }) }), }); }); if (options.preparingRoute !== undefined) { @@ -268,6 +207,9 @@ async function startAllocation( } await stub.ensureAllocation({ provider: options.provider ?? 'cloudflare', + ...(options.provider === 'vercel' + ? { byoc: { userId: 'owner-1', credentialId: '22222222-2222-4222-8222-222222222222' } } + : {}), ...(options.configuration === undefined ? {} : { configuration: options.configuration }), allocationName: options.allocationName ?? sandboxId, ...(options.billing === undefined ? {} : { billing: options.billing }), @@ -474,6 +416,25 @@ afterEach(async () => { }); describe('SandboxControlV2 allocation lifecycle', () => { + it('uses a new durable Vercel sandbox name for each allocation', async () => { + const provider = createFakeProvider(); + const stub = await startAllocation(provider, { provider: 'vercel' }); + await awaitStarting(provider, stub); + const first = provider.createInputs[0]; + if (!first) throw new Error('Missing first Vercel create'); + await stub.reportProviderGone(); + await waitFor(async () => expect((await readState(stub)).kind).toBe('stopped')); + await startAllocation(provider, { provider: 'vercel' }); + await waitFor(() => expect(provider.createInputs).toHaveLength(2)); + const second = provider.createInputs[1]; + expect(second?.allocationName).not.toBe(first.allocationName); + const saved = JSON.parse((await readProviderPin(stub)) ?? '{}'); + expect(saved.allocationName).toBe(second?.allocationName); + await evictAllDurableObjects(); + expect(JSON.parse((await readProviderPin(stub)) ?? '{}').allocationName).toBe( + second?.allocationName + ); + }); it.each([ { provider: 'cloudflare' }, { provider: 'cloudflare-containers' }, @@ -645,68 +606,24 @@ describe('SandboxControlV2 allocation lifecycle', () => { ); }); - it.each(['cloudflare', 'cloudflare-containers', 'vercel'] as const)( - 'does not call the %s provider when database admission is unavailable', - async providerKind => { - const sandboxId = `ses-${'b'.repeat(48)}`; - const sessionId = 'workspace_11111111-1111-4111-8111-111111111111'; - const stub = sandboxNamespace.getByName(sandboxId); - await runInDurableObject(stub, instance => { - Object.assign(instance, { - env: { - ...instance.env, - CLOUD_AGENT_CONTAINER_CONCURRENCY_DISABLED: 'false', - HYPERDRIVE: undefined, - }, - }); - }); - const provider = createFakeProvider(); - await startAllocation(provider, { - sandboxId, - provider: providerKind, - preparingRoute: sessionId, - billing: { - sandboxId, - subject: { type: 'user', id: 'owner-1' }, - actor: { type: 'user', id: 'owner-1' }, - sessionId, - metadata: { origin: 'cloud-agent' }, - }, - }); - await waitFor(async () => - expect((await stub.status({ sessionId })).view).toMatchObject({ - state: 'failed', - reason: 'billing_unavailable', - }) - ); - expect((await readState(stub)).kind).not.toBe('creating'); - expect(provider.createCalls).toBe(0); - expect(provider.launchEnvs).toHaveLength(0); - } - ); - - it.each([ - [new ContainerConcurrencyLimitError('personal', 20), 'container_limit_reached'], - [new ContainerCapacityUnavailableError(), 'billing_unavailable'], - ] as const)( - 'fails capacity denial from the sandbox start without retrying creation: %s', - async (failure, reason) => { - const provider = createFakeProvider({ gateLaunch: true }); - const stub = await startAllocation(provider, { preparingRoute: 'waiting' }); - await waitFor(() => expect(provider.launchGates).toHaveLength(1)); - await releaseGate(stub, () => - provider.launchGates[0](new Error(`remote: ${failure.message}`)) - ); - await waitFor(async () => - expect((await stub.status({ sessionId: 'waiting' })).view).toMatchObject({ - state: 'failed', - reason, - }) - ); - await waitFor(async () => expect((await readState(stub)).kind).not.toBe('creating')); - expect(provider.createCalls).toBe(1); - } - ); + it('fails a container limit denial from the sandbox start without retrying the create', async () => { + const provider = createFakeProvider({ gateLaunch: true }); + const stub = await startAllocation(provider, { preparingRoute: 'waiting' }); + await waitFor(() => expect(provider.launchGates).toHaveLength(1)); + await releaseGate(stub, () => + provider.launchGates[0]( + new Error(`remote: ${new ContainerConcurrencyLimitError('personal', 20).message}`) + ) + ); + await waitFor(async () => + expect((await stub.status({ sessionId: 'waiting' })).view).toMatchObject({ + state: 'failed', + reason: 'container_limit_reached', + }) + ); + await waitFor(async () => expect((await readState(stub)).kind).not.toBe('creating')); + expect(provider.createCalls).toBe(1); + }); it('ignores a permanent late launch rejection after hello has acquired the allocation', async () => { const provider = createFakeProvider({ gateLaunch: true }); @@ -1749,11 +1666,7 @@ describe('SandboxControlV2 allocation lifecycle', () => { provider: configuration.provider, configuration, }); - if (configuration.provider === 'cloudflare-containers') { - await awaitStarting(provider, stub); - } else { - await waitFor(async () => expect((await stub.getAllocationState()).kind).toBe('stopped')); - } + await awaitStarting(provider, stub); expect((await stub.getAllocationState()).configuration).toEqual(configuration); await evictAllDurableObjects(); expect((await stub.getAllocationState()).configuration).toEqual(configuration); @@ -1931,7 +1844,7 @@ describe('SandboxControlV2 allocation lifecycle', () => { expect(provider.stopCalls).toHaveLength(maxAttempts); }); - it('preserves an unsettled billing continuation during the legacy storage cutover', async () => { + it('preserves retired Vercel billing history without rearming its continuation during legacy cutover', async () => { const sandboxId = `ses-${'e'.repeat(48)}`; let stub = sandboxNamespace.getByName(sandboxId); const billing = { @@ -1965,7 +1878,7 @@ describe('SandboxControlV2 allocation lifecycle', () => { })); expect(retained.schedule?.[VERCEL_BILLING_SETTLEMENT_CALLBACK]?.dueAtMs).toBe(dueAtMs); expect(retained.acknowledgement).toBe('generation-1'); - expect(await readAlarm(stub)).toBe(dueAtMs); + expect(await readAlarm(stub)).toBeNull(); expect(JSON.parse((await readProviderPin(stub)) ?? '{}').billing).toMatchObject(billing); }); @@ -1984,985 +1897,29 @@ describe('SandboxControlV2 allocation lifecycle', () => { expect(JSON.parse((await readProviderPin(stub)) ?? '{}').billing).toBeNull(); }); - it.each(['acked', 'insufficient', 'uncertain', 'budget'] as const)( - 'admits Vercel create only after %s billing admission settles', - async result => { - const sandboxId = `ses-${(result === 'acked' ? 'a' : result === 'insufficient' ? 'b' : result === 'budget' ? 'd' : 'c').repeat(48)}`; - const provider = createFakeProvider({ gateStop: result === 'budget' }); - const providerRef = encodeVercelProviderRef({ sandboxName: sandboxId, sessionId: 'vsess_1' }); - provider.adapter.create = async intent => { - provider.createCalls++; - provider.createInputs.push(intent); - return { providerRef }; - }; - const starts: string[] = []; - const stops: number[] = []; - const heartbeats: number[] = []; - const meter: ContainerUsageRpcMethods = { - async recordStart(input) { - starts.push(input.instanceId); - if (result === 'uncertain') throw new Error('Meter unavailable'); - if (result === 'insufficient') - return { - success: false, - error: { code: 'insufficient_credits', message: 'No credits' }, - }; - return { success: true, ack: { intervalId: 'interval-1', durable: 'pg', dedup: false } }; - }, - async recordHeartbeat(input) { - heartbeats.push(input.usageSinceLast); - return { - intervalId: 'interval-1', - durable: 'pg', - dedup: false, - budget: { verdict: result === 'budget' ? 'stop' : 'continue' }, - }; - }, - async recordStop(input) { - stops.push(input.usageSinceLast); - return { intervalId: 'interval-1', durable: 'pg', dedup: false }; - }, - }; - const stub = await startAllocation(provider, { - sandboxId, - provider: 'vercel', - configuration: { provider: 'vercel', resources: { vcpus: 2, memory: 4096 } }, - billing: { - sandboxId, - subject: { type: 'user', id: 'owner-1' }, - actor: { type: 'user', id: 'owner-1' }, - sessionId: 'workspace_11111111-1111-4111-8111-111111111111', - metadata: { origin: 'cloud-agent' }, - enforcementRequested: true, - }, - meter, - ...(result === 'insufficient' - ? { preparingRoute: 'workspace_11111111-1111-4111-8111-111111111111' } - : {}), - }); - await waitFor(() => expect(starts).toContain(sandboxId)); - if (result === 'acked' || result === 'budget') { - await waitFor(() => expect(provider.createCalls).toBe(1)); - } else { - await waitFor(async () => expect((await readState(stub)).kind).toBe('stopped')); - expect(provider.createCalls).toBe(0); - } - const billing = await runInDurableObject(stub, async (_instance, state) => ({ - context: await getBillingContext(state.storage), - schedule: await state.storage.get(VERCEL_BILLING_SCHEDULE_KEY), - })); - expect(billing.context === undefined).toBe(result === 'insufficient'); - expect(billing.schedule?.[VERCEL_BILLING_SETTLEMENT_CALLBACK] !== undefined).toBe( - result !== 'insufficient' - ); - if (result === 'insufficient') { - const row = await runInDurableObject(stub, async (_instance, state) => { - const db = drizzle(state.storage, { logger: false }); - return db.select().from(routesTable); - }); - expect(row[0]).toMatchObject({ state: 'failed', reason: 'billing_blocked' }); - } - if (result === 'acked' || result === 'budget') { - const createdAtMs = Date.now() - 20_000; - const terminalAtMs = createdAtMs + 10_000; - await runInDurableObject(stub, instance => - ( - instance as unknown as { - recordVercelBillingLifetime: (evidence: { - providerRef: string; - createdAtMs: number; - }) => Promise; - } - ).recordVercelBillingLifetime({ providerRef, createdAtMs }) - ); - await stub.ensureAllocation(); - await runInDurableObject(stub, async instance => { - const context = await getBillingContext(instance.ctx.storage); - if (context === undefined) throw new Error('Billing interval was not opened'); - await ( - instance as unknown as { - billingSchedule: { - schedule(callback: string, at: number, generation: string): Promise; - }; - } - ).billingSchedule.schedule( - VERCEL_BILLING_SETTLEMENT_CALLBACK, - Date.now() - 1, - context.generation - ); - }); - await runAlarm(stub); - await waitFor(() => expect(heartbeats).toHaveLength(1)); - expect(heartbeats[0]).toBeGreaterThanOrEqual(19); - if (result === 'budget') { - await waitFor(() => expect(provider.stopCalls).toContain(providerRef)); - const generation = billing.context?.generation; - if (generation === undefined) throw new Error('Expected active budget-stop generation'); - await runInDurableObject(stub, instance => - ( - instance as unknown as { - billingSchedule: { - schedule(callback: string, at: number, generation: string): Promise; - }; - } - ).billingSchedule.schedule('billingForceStop', Date.now() - 1, generation) - ); - await runAlarm(stub); - const forceSchedule = await runInDurableObject(stub, (_instance, state) => - state.storage.get(VERCEL_BILLING_SCHEDULE_KEY) - ); - expect(forceSchedule?.billingForceStop).toBeDefined(); - await releaseGate(stub, () => provider.stopGates.shift()?.('terminal')); - await waitFor(async () => { - expect( - await runInDurableObject(stub, (_instance, state) => getBillingContext(state.storage)) - ).toBeUndefined(); - }); - return; - } - await runInDurableObject(stub, instance => - ( - instance as unknown as { - recordVercelBillingLifetime: (evidence: { - providerRef: string; - createdAtMs: number; - terminalAtMs: number; - }) => Promise; - } - ).recordVercelBillingLifetime({ providerRef, createdAtMs, terminalAtMs }) - ); - await runAlarm(stub); - await waitFor(() => expect(stops).toHaveLength(1)); - expect(stops[0]).toBe(0); - await waitFor(async () => { - const context = await runInDurableObject(stub, (_instance, state) => - getBillingContext(state.storage) - ); - expect(context).toBeUndefined(); - }); - } - } - ); - - it('settles an uncertain pre-create generation before retrying a preparing route', async () => { - const sandboxId = `ses-${'f'.repeat(48)}`; - const sessionId = 'workspace_11111111-1111-4111-8111-111111111111'; - let starts = 0; - let stops = 0; - let meterUnavailable = true; - const meter: ContainerUsageRpcMethods = { - async recordStart(input) { - starts++; - if (meterUnavailable) throw new Error('Meter temporarily unavailable'); - return { - success: true, - ack: { intervalId: `${input.startEpochMs}`, durable: 'pg', dedup: false }, - }; - }, - async recordHeartbeat() { - return { - intervalId: 'interval-1', - durable: 'pg', - dedup: false, - budget: { verdict: 'continue' }, - }; - }, - async recordStop() { - stops++; - return { intervalId: 'interval-1', durable: 'pg', dedup: false }; - }, - }; - const provider = createFakeProvider(); - const stub = await startAllocation(provider, { - sandboxId, - provider: 'vercel', - configuration: { provider: 'vercel', resources: { vcpus: 2, memory: 4096 } }, - billing: { - sandboxId, - subject: { type: 'user', id: 'owner-1' }, - actor: { type: 'user', id: 'owner-1' }, - sessionId, - metadata: { origin: 'cloud-agent' }, - enforcementRequested: true, - }, - meter, - preparingRoute: sessionId, - }); - await waitFor(() => expect(starts).toBeGreaterThanOrEqual(1)); - await waitFor(async () => expect((await readState(stub)).kind).not.toBe('stopped')); - await waitFor(async () => - expect( - await runInDurableObject(stub, instance => - Promise.resolve((instance as unknown as { createInFlight: boolean }).createInFlight) - ) - ).toBe(false) - ); - const generation = await runInDurableObject(stub, (_instance, state) => - getBillingContext(state.storage) - ); - if (generation === undefined) throw new Error('Expected an uncertain billing generation'); - expect(generation.measurementStarted).toBe(false); - expect(provider.createCalls).toBe(0); - meterUnavailable = false; - await runInDurableObject(stub, async (instance, state) => { - const current = await getBillingContext(state.storage); - if (current?.generation !== generation.generation) return; - await ( - instance as unknown as { - billingSchedule: { - schedule(callback: string, at: number, generation: string): Promise; - }; - } - ).billingSchedule.schedule( - VERCEL_BILLING_SETTLEMENT_CALLBACK, - Date.now() - 1, - generation.generation - ); - }); - await runAlarm(stub); - await waitFor(() => expect(stops).toBeGreaterThanOrEqual(1)); - await waitFor(async () => { - const current = await runInDurableObject(stub, (_instance, state) => - getBillingContext(state.storage) - ); - expect(current?.generation).not.toBe(generation.generation); - }); - if (provider.createCalls === 0) { - await setDeadline(stub, { create_deadline_at: Date.now() - 1 }); - await runAlarm(stub); - } - await waitFor(() => expect(provider.createCalls).toBe(1)); - expect(starts).toBeGreaterThan(1); - }, 20_000); - - it.each(['disconnected', 'stopping'] as const)( - 'retires a %s allocation with missing billing while preserving its preparing route', - async existingState => { - const sandboxId = `ses-${'d'.repeat(47)}${existingState === 'stopping' ? 'b' : 'a'}`; - const meter: ContainerUsageRpcMethods = { - async recordStart() { - return { success: true, ack: { intervalId: 'interval-1', durable: 'pg', dedup: false } }; - }, - async recordHeartbeat() { - return { - intervalId: 'interval-1', - durable: 'pg', - dedup: false, - budget: { verdict: 'continue' }, - }; - }, - async recordStop() { - return { intervalId: 'interval-1', durable: 'pg', dedup: false }; - }, - }; - const provider = createFakeProvider({ gateStop: existingState === 'stopping' }); - provider.adapter.applyContainedCredentials = async ref => { - if (ref === provider.refs[0]) throw new Error('Old sandbox is retiring'); - }; - const stub = await startAllocation(provider, { - sandboxId, - provider: 'vercel', - configuration: { provider: 'vercel', resources: { vcpus: 2, memory: 4096 } }, - billing: { - sandboxId, - subject: { type: 'user', id: 'owner-1' }, - actor: { type: 'user', id: 'owner-1' }, - sessionId: 'workspace_11111111-1111-4111-8111-111111111111', - metadata: { origin: 'cloud-agent' }, - enforcementRequested: true, - }, - meter, - }); - await awaitStarting(provider, stub); - if (existingState === 'disconnected') { - await runInDurableObject(stub, async instance => { - const state = await instance.getAllocationState(); - if (state.allocationId === null) throw new Error('Missing allocation'); - const applyEvent = ( - instance as unknown as { - applyEvent(event: { - type: 'hello-accepted' | 'socket-closed'; - at: number; - allocationId: string; - connectionId: string; - wrapperId?: string; - origin?: 'peer' | 'heartbeat_timeout'; - }): Promise; - } - ).applyEvent.bind(instance); - await applyEvent({ - type: 'hello-accepted', - at: Date.now(), - allocationId: state.allocationId, - connectionId: 'conn-1', - wrapperId: 'wr-1', - }); - await applyEvent({ - type: 'socket-closed', - at: Date.now(), - allocationId: state.allocationId, - connectionId: 'conn-1', - origin: 'peer', - }); - }); - } else { - await runInDurableObject(stub, instance => - ( - instance as unknown as { - applyEvent(event: { - type: 'stop-requested'; - at: number; - reason: 'sandbox_stopped'; - }): Promise; - } - ).applyEvent({ type: 'stop-requested', at: Date.now(), reason: 'sandbox_stopped' }) - ); - await waitFor(() => expect(provider.stopGates).toHaveLength(1)); - } - await runInDurableObject(stub, (_instance, state) => clearBillingContext(state.storage)); - await insertPreparingRoute(stub, { - sessionId: 'workspace_missing_billing', - attemptDeadlineAt: Date.now() + MINUTE, - }); - const stopReasons: string[] = []; - await runInDurableObject(stub, instance => { - const original = ( - instance as unknown as { - applyEvent(event: { type: string; reason?: string }): Promise; - } - ).applyEvent.bind(instance); - Object.assign(instance, { - applyEvent: async (event: { type: string; reason?: string }) => { - if (event.type === 'stop-requested' && event.reason !== undefined) - stopReasons.push(event.reason); - await original(event); - }, - }); - return Promise.resolve(); - }); - const result = await stub.prepare({ - spec: { - sessionId: 'workspace_missing_billing', - kiloSessionId: 'kilo-missing-billing', - directory: '/workspace/test', - attemptId: 'attempt-1', - }, - credentials: { - userId: 'owner-1', - kiloSessionId: 'kilo-missing-billing', - kiloToken: 'test-token', - scopeId: 'scope-1', - }, - }); - expect(result).toMatchObject({ state: 'preparing' }); - if (existingState === 'disconnected') expect(stopReasons).toContain('sandbox_lost'); - if (existingState === 'stopping') { - await releaseGate(stub, () => provider.stopGates.shift()?.('terminal')); - } - await waitFor(() => expect(provider.createCalls).toBe(2)); - const route = await runInDurableObject(stub, async (_instance, state) => { - const db = drizzle(state.storage, { logger: false }); - return db - .select() - .from(routesTable) - .where(eq(routesTable.session_id, 'workspace_missing_billing')); - }); - expect(route[0]).toMatchObject({ state: 'preparing', reason: null }); - }, - 20_000 - ); - - it('reads current stored grants after eviction and adapter construction, excluding expired grants', async () => { - // The DO id must be a valid sandbox id (legacy-shared `__` shape); the - // allocation name is what the real Vercel REST client validates. - const sandboxId = 'sbx__chunk3_grant_reader'; - const allocationName = 'ses-chunk3-grant-reader'; - const stub = sandboxNamespace.getByName(sandboxId); - const now = Date.now(); - const active = containedGrant(sandboxId, { - scopeId: 'scope-active', - handle: 'handle_active', - preparedAt: now, - expiresAt: now + MINUTE, - }); - const expired = containedGrant(sandboxId, { - scopeId: 'scope-expired', - handle: 'handle_expired', - preparedAt: now - 2 * MINUTE, - expiresAt: now - MINUTE, - }); - const next = containedGrant(sandboxId, { - scopeId: 'scope-next', - handle: 'handle_next', - preparedAt: now, - expiresAt: now + MINUTE, - }); - const captured: string[] = []; - const fetchStub = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { - captured.push(typeof init?.body === 'string' ? init.body : ''); - return new Response('provider error', { status: 500 }); - }); - const vercelEnv = { - VERCEL_TOKEN: 'vercel-token', - VERCEL_TEAM_ID: 'team_1', - VERCEL_PROJECT_ID: 'prj_1', - VERCEL_SANDBOX_SNAPSHOT_ID: 'snap_1', - VERCEL_SANDBOX_RUNTIME_BUILD_ID: 'build_1', - VERCEL_SANDBOX_RUNTIME: 'node24', - VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: '600000', - VERCEL_SANDBOX_EXTEND_DURATION_MS: '600000', - }; - - await runInDurableObject(stub, async instance => { - await instance.getAllocationState(); - Object.assign(instance, { env: { ...instance.env, ...vercelEnv } }); - vi.stubGlobal('fetch', fetchStub); - }); - - // With no owner, admission is unavailable: this persists the Vercel pin - // without a provider create. - await stub.ensureAllocation({ - provider: 'vercel', - configuration: { provider: 'vercel' }, - allocationName, - }); - await waitFor(async () => expect((await stub.getAllocationState()).kind).toBe('stopped')); - expect(captured).toHaveLength(0); - - await runInDurableObject(stub, async (_instance, state) => { - await state.storage.put('control_plane_owner', 'owner-1'); - const db = drizzle(state.storage, { logger: false }); - writeScopeGrant(db, active); - writeScopeGrant(db, expired); - }); - - await evictDurableObject(stub); - - await runInDurableObject(stub, async (instance, state) => { - await instance.getAllocationState(); - Object.assign(instance, { env: { ...instance.env, ...vercelEnv } }); - const db = drizzle(state.storage, { logger: false }); - const realFactory = ( - instance as unknown as { - createProviderAdapter(pin: StoredProviderPin): ProviderAdapter; - } - ).createProviderAdapter.bind(instance); - Object.assign(instance, { - createProviderAdapter(pin: StoredProviderPin) { - expect(listScopeGrants(db)).toEqual(expect.arrayContaining([active, expired])); - const adapter = realFactory(pin); - for (const grant of listScopeGrants(db)) { - writeScopeGrant(db, { - ...grant, - preparedAt: now - 2 * MINUTE, - expiresAt: now - 1, - }); - } - writeScopeGrant(db, next); - return adapter; - }, - }); - captured.length = 0; - }); - - await stub.ensureAllocation({ - provider: 'vercel', - configuration: { provider: 'vercel' }, - allocationName, - }); - await waitFor(() => expect(captured.length).toBeGreaterThan(0)); - - const body = JSON.parse(captured[0] ?? '{}') as { networkPolicy: unknown }; - expect(body.networkPolicy).toEqual(buildControlNetworkPolicy([next])); - expect(JSON.stringify(body.networkPolicy)).toContain('handle_next'); - expect(JSON.stringify(body.networkPolicy)).not.toContain('handle_active'); - expect(JSON.stringify(body.networkPolicy)).not.toContain('handle_expired'); - }, 20_000); - - it('rejects a blocked billing route before sending wrapper preparation', async () => { - const sandboxId = `ses-${'d'.repeat(47)}c`; - const meter: ContainerUsageRpcMethods = { - async recordStart() { - return { success: true, ack: { intervalId: 'interval-1', durable: 'pg', dedup: false } }; - }, - async recordHeartbeat() { - return { - intervalId: 'interval-1', - durable: 'pg', - dedup: false, - budget: { verdict: 'continue' }, - }; - }, - async recordStop() { - return { intervalId: 'interval-1', durable: 'pg', dedup: false }; - }, - }; - const provider = createFakeProvider(); - const stub = await startAllocation(provider, { - sandboxId, - provider: 'vercel', - configuration: { provider: 'vercel', resources: { vcpus: 2, memory: 4096 } }, - billing: { - sandboxId, - subject: { type: 'user', id: 'owner-1' }, - actor: { type: 'user', id: 'owner-1' }, - sessionId: 'workspace_11111111-1111-4111-8111-111111111111', - metadata: { origin: 'cloud-agent' }, - enforcementRequested: true, - }, - meter, - }); - await awaitStarting(provider, stub); - const sent: string[] = []; - await runInDurableObject(stub, async instance => { - const control = instance as unknown as { - ensureVercelBillingRuntime(): Promise< - { lifecycle: { isBillingBlocked(): Promise } } | undefined - >; - }; - const runtime = await control.ensureVercelBillingRuntime(); - if (runtime === undefined) throw new Error('missing Vercel billing runtime'); - Object.assign(runtime.lifecycle, { isBillingBlocked: async () => true }); - Object.assign(instance, { sendSessionPrepare: () => sent.push('prepare') }); - }); - const result = await stub.prepare({ - spec: { - sessionId: 'workspace_blocked_billing', - kiloSessionId: 'kilo-blocked', - directory: '/workspace/test', - attemptId: 'attempt-1', - }, - credentials: { - userId: 'owner-1', - kiloSessionId: 'kilo-blocked', - kiloToken: 'test-token', - scopeId: 'scope-1', - }, - }); - expect(result).toMatchObject({ state: 'failed', reason: 'billing_blocked' }); - expect(sent).toEqual([]); - const route = await runInDurableObject(stub, async (_instance, state) => { - const db = drizzle(state.storage, { logger: false }); - return db - .select() - .from(routesTable) - .where(eq(routesTable.session_id, 'workspace_blocked_billing')); - }); - expect(route[0]).toMatchObject({ state: 'failed', reason: 'billing_blocked' }); - }); - - it.each([ - ['Invalid billing state', 'failed', 'billing_unavailable'], - ['Vercel billing admission timed out', 'failed', 'billing_unavailable'], - ['hang', 'preparing', null], - ] as const)( - 'classifies thrown admission error %s without masking a definite failure as retry', - async (error, routeState, reason) => { - const sandboxId = `ses-${(error === 'hang' ? 'b' : error === 'Invalid billing state' ? 'a' : 'c').repeat(47)}d`; - const provider = createFakeProvider(); - const meter: ContainerUsageRpcMethods = { - async recordStart() { - throw new Error('The admission override should run'); - }, - async recordHeartbeat() { - throw new Error('The admission override should run'); - }, - async recordStop() { - throw new Error('The admission override should run'); - }, - }; - const stub = await startAllocation(provider, { - sandboxId, - provider: 'vercel', - configuration: { provider: 'vercel', resources: { vcpus: 2, memory: 4096 } }, - billing: { - sandboxId, - subject: { type: 'user', id: 'owner-1' }, - actor: { type: 'user', id: 'owner-1' }, - sessionId: 'workspace_11111111-1111-4111-8111-111111111111', - metadata: { origin: 'cloud-agent' }, - enforcementRequested: true, - }, - meter, - preparingRoute: 'workspace_admission_error', - admissionError: error, - }); - await waitFor(async () => { - const rows = await runInDurableObject(stub, async (_instance, state) => { - const db = drizzle(state.storage, { logger: false }); - return db - .select() - .from(routesTable) - .where(eq(routesTable.session_id, 'workspace_admission_error')); - }); - expect(rows[0]).toMatchObject({ state: routeState, reason }); - }); - expect(provider.createCalls).toBe(0); - expect((await readState(stub)).kind).toBe(routeState === 'failed' ? 'stopped' : 'creating'); - } - ); - - it('expires a route even when billing continuation fails', async () => { - const provider = createFakeProvider(); - const stub = await startAllocation(provider); - await awaitStarting(provider, stub); - await insertPreparingRoute(stub, { - sessionId: 'workspace_alarm', - attemptDeadlineAt: Date.now() - 1, - }); - await runInDurableObject(stub, instance => { - Object.assign(instance, { - runBillingAlarm: async () => { - throw new Error('Meter unavailable'); - }, - }); - return Promise.resolve(); - }); - await runAlarm(stub); - const row = await runInDurableObject(stub, async (_instance, state) => { - const db = drizzle(state.storage, { logger: false }); - return db.select().from(routesTable).where(eq(routesTable.session_id, 'workspace_alarm')); + it('retries a transient launch failure after the retry pause', async () => { + const provider = createFakeProvider({ + failFirstLaunch: 'runtime bootstrap failed without a phase receipt', }); - expect(row[0]).toMatchObject({ state: 'failed', reason: 'preparation_timeout' }); - }); - - it.each(['provider-gone', 'confirmed-stop'] as const)( - 'settles measured billing before reallocating a preparing route after %s', - async termination => { - const sandboxId = `ses-${(termination === 'provider-gone' ? 'b' : 'c').repeat(47)}a`; - let stops = 0; - const meter: ContainerUsageRpcMethods = { - async recordStart() { - return { success: true, ack: { intervalId: 'interval-1', durable: 'pg', dedup: false } }; - }, - async recordHeartbeat() { - return { - intervalId: 'interval-1', - durable: 'pg', - dedup: false, - budget: { verdict: 'continue' }, - }; - }, - async recordStop() { - stops++; - return { intervalId: 'interval-1', durable: 'pg', dedup: false }; - }, - }; - const provider = createFakeProvider(); - const stub = await startAllocation(provider, { - sandboxId, - provider: 'vercel', - configuration: { provider: 'vercel', resources: { vcpus: 2, memory: 4096 } }, - billing: { - sandboxId, - subject: { type: 'user', id: 'owner-1' }, - actor: { type: 'user', id: 'owner-1' }, - sessionId: 'workspace_11111111-1111-4111-8111-111111111111', - metadata: { origin: 'cloud-agent' }, - enforcementRequested: true, - }, - meter, - }); - await awaitStarting(provider, stub); - const providerRef = provider.refs[0]; - if (providerRef === undefined) throw new Error('Missing created provider reference'); - await runInDurableObject(stub, instance => - ( - instance as unknown as { - recordVercelBillingLifetime(evidence: { - providerRef: string; - createdAtMs: number; - }): Promise; - } - ).recordVercelBillingLifetime({ providerRef, createdAtMs: Date.now() - 20_000 }) - ); - await stub.ensureAllocation(); - await waitFor(async () => { - const context = await runInDurableObject(stub, (_instance, state) => - getBillingContext(state.storage) - ); - expect(context?.measurementStarted).toBe(true); - }); - await insertPreparingRoute(stub, { - sessionId: 'workspace_recover', - attemptDeadlineAt: Date.now() + MINUTE, - }); - if (termination === 'provider-gone') { - await stub.reportProviderGone(); - } else { - await runInDurableObject(stub, instance => - ( - instance as unknown as { - applyEvent(event: { - type: 'stop-requested'; - at: number; - reason: 'sandbox_stopped'; - }): Promise; - } - ).applyEvent({ type: 'stop-requested', at: Date.now(), reason: 'sandbox_stopped' }) - ); - } - await waitFor(() => expect(stops).toBeGreaterThanOrEqual(1)); - if (provider.createCalls === 1) { - await setDeadline(stub, { create_deadline_at: Date.now() - 1 }); - await runAlarm(stub); - } - await waitFor(() => expect(provider.createCalls).toBe(2)); - }, - 20_000 - ); - - it.each(['stop', 'observation'] as const)( - 'settles a bound Vercel ref after launch fails via %s before retrying its route', - async terminalEvidence => { - const sandboxId = `ses-${(terminalEvidence === 'stop' ? 'a' : 'b').repeat(47)}e`; - let stops = 0; - const meter: ContainerUsageRpcMethods = { - async recordStart() { - return { success: true, ack: { intervalId: 'interval-1', durable: 'pg', dedup: false } }; - }, - async recordHeartbeat() { - return { - intervalId: 'interval-1', - durable: 'pg', - dedup: false, - budget: { verdict: 'continue' }, - }; - }, - async recordStop() { - stops++; - return { intervalId: 'interval-1', durable: 'pg', dedup: false }; - }, - }; - const provider = createFakeProvider({ gateLaunch: true }); - provider.stopResults.push('retryable', 'terminal'); - if (terminalEvidence === 'observation') { - provider.adapter.observe = async () => ({ status: 'terminal' }); - } - const stub = await startAllocation(provider, { - sandboxId, - provider: 'vercel', - configuration: { provider: 'vercel', resources: { vcpus: 2, memory: 4096 } }, - billing: { - sandboxId, - subject: { type: 'user', id: 'owner-1' }, - actor: { type: 'user', id: 'owner-1' }, - sessionId: 'workspace_11111111-1111-4111-8111-111111111111', - metadata: { origin: 'cloud-agent' }, - enforcementRequested: true, - }, - meter, - preparingRoute: 'workspace_retry_launch', - }); - await awaitLaunch(provider); - const ref = provider.refs[0]; - if (ref === undefined) throw new Error('Expected a created Vercel ref'); - await runInDurableObject(stub, instance => - ( - instance as unknown as { - recordVercelBillingLifetime(evidence: { - providerRef: string; - createdAtMs: number; - }): Promise; - } - ).recordVercelBillingLifetime({ providerRef: ref, createdAtMs: Date.now() - 20_000 }) - ); - await releaseGate(stub, () => provider.launchGates.shift()?.(new Error('launch failed'))); - await waitFor(() => expect(provider.stopCalls).toEqual([ref])); - await waitFor(async () => expect((await readState(stub)).kind).toBe('creating')); - const billing = await runInDurableObject(stub, (_instance, state) => - getBillingContext(state.storage) - ); - expect(billing?.pendingStop).toBeUndefined(); - expect(provider.createCalls).toBe(1); - if (billing === undefined) throw new Error('Expected unsettled generation'); - await runInDurableObject(stub, instance => - ( - instance as unknown as { - billingSchedule: { - schedule(callback: string, at: number, generation: string): Promise; - }; - } - ).billingSchedule.schedule( - VERCEL_BILLING_SETTLEMENT_CALLBACK, - Date.now() - 1, - billing.generation - ) - ); - await runAlarm(stub); - await waitFor(() => - expect(provider.stopCalls).toEqual(terminalEvidence === 'stop' ? [ref, ref] : [ref]) - ); - await waitFor(() => expect(stops).toBe(1)); - if (provider.createCalls === 1) { - await setDeadline(stub, { create_deadline_at: Date.now() - 1 }); - await runAlarm(stub); - } - await waitFor(() => expect(provider.createCalls).toBe(2)); - }, - 20_000 - ); - - it('fails waiting routes when cleanup is unconfirmed but retains its billing alarm', async () => { - const sandboxId = `ses-${'c'.repeat(47)}e`; - let meterStops = 0; - const meter: ContainerUsageRpcMethods = { - async recordStart() { - return { success: true, ack: { intervalId: 'interval-1', durable: 'pg', dedup: false } }; - }, - async recordHeartbeat() { - return { - intervalId: 'interval-1', - durable: 'pg', - dedup: false, - budget: { verdict: 'continue' }, - }; - }, - async recordStop() { - meterStops++; - return { intervalId: 'interval-1', durable: 'pg', dedup: false }; - }, - }; - const provider = createFakeProvider({ gateLaunch: true }); - provider.adapter.stop = async ref => { - provider.stopCalls.push(ref); - return 'retryable'; - }; const stub = await startAllocation(provider, { - sandboxId, - provider: 'vercel', - configuration: { provider: 'vercel', resources: { vcpus: 2, memory: 4096 } }, - billing: { - sandboxId, - subject: { type: 'user', id: 'owner-1' }, - actor: { type: 'user', id: 'owner-1' }, - sessionId: 'workspace_11111111-1111-4111-8111-111111111111', - metadata: { origin: 'cloud-agent' }, - enforcementRequested: true, - }, - meter, - preparingRoute: 'workspace_pending_cleanup', + preparingRoute: 'workspace_cold_retry', + timers: { providerCreateRetryMs: 1 }, }); - await awaitLaunch(provider); - const ref = provider.refs[0]; - if (ref === undefined) throw new Error('Expected created ref'); - await runInDurableObject(stub, instance => - ( - instance as unknown as { - recordVercelBillingLifetime(evidence: { - providerRef: string; - createdAtMs: number; - }): Promise; - } - ).recordVercelBillingLifetime({ providerRef: ref, createdAtMs: Date.now() - 20_000 }) - ); - await releaseGate(stub, () => provider.launchGates.shift()?.(new Error('launch failed'))); - await waitFor(() => expect(provider.stopCalls).toEqual([ref])); - const generation = await runInDurableObject(stub, async (_instance, state) => { - const context = await getBillingContext(state.storage); - if (context === undefined) throw new Error('Missing bound generation'); - await updateBillingContext(state.storage, { - ...context, - startEpochMs: Date.now() - TIMERS.providerCreateMs - 1, - }); - return context.generation; + await waitFor(() => expect(provider.launchEnvs).toHaveLength(1)); + const firstAttempt = launchIdentity(provider); + // The first attempt fails after committing its ref; cleanup stops that ref + // and the reducer enters the short retry pause with a null ref. + await waitFor(async () => { + const state = await readState(stub); + expect(state.allocationId).not.toBe(firstAttempt.allocationId); + expect(state.providerRef).toBeNull(); }); - const scheduleCleanup = () => - runInDurableObject(stub, instance => - ( - instance as unknown as { - billingSchedule: { - schedule(callback: string, at: number, generation: string): Promise; - }; - } - ).billingSchedule.schedule(VERCEL_BILLING_SETTLEMENT_CALLBACK, Date.now() - 1, generation) - ); - await scheduleCleanup(); await runAlarm(stub); - const route = await runInDurableObject(stub, async (_instance, state) => { - const db = drizzle(state.storage, { logger: false }); - return db - .select() - .from(routesTable) - .where(eq(routesTable.session_id, 'workspace_pending_cleanup')); - }); - expect(route[0]).toMatchObject({ state: 'failed', reason: 'sandbox_lost' }); - expect((await readState(stub)).kind).toBe('stopped'); - expect( - (await runInDurableObject(stub, (_instance, state) => getBillingContext(state.storage))) - ?.pendingStop - ).toBeUndefined(); - expect(meterStops).toBe(0); - expect(provider.stopCalls).toEqual([ref, ref]); - const schedule = await runInDurableObject(stub, (_instance, state) => - state.storage.get(VERCEL_BILLING_SCHEDULE_KEY) - ); - const continuation = schedule?.[VERCEL_BILLING_SETTLEMENT_CALLBACK]; - expect(continuation?.payload).toBe(generation); - expect(continuation?.dueAtMs).toBeGreaterThan(Date.now()); - expect(meterStops).toBe(0); + // A transient launch failure must retain the retry: a second attempt starts. + await waitFor(() => expect(provider.createCalls).toBe(2)); + const view = (await stub.status({ sessionId: 'workspace_cold_retry' })).view; + expect(view.state).not.toBe('failed'); }, 20_000); - - it.each(['valid', 'wrong-owner', 'invalid-size'] as const)( - 'handles a shadow admission exception with %s billing identity', - async identity => { - const sandboxId = `ses-${(identity === 'valid' ? 'b' : identity === 'wrong-owner' ? 'c' : 'd').repeat(47)}e`; - const meter: ContainerUsageRpcMethods = { - async recordStart() { - return { success: true, ack: { intervalId: 'interval-1', durable: 'pg', dedup: false } }; - }, - async recordHeartbeat() { - return { - intervalId: 'interval-1', - durable: 'pg', - dedup: false, - budget: { verdict: 'continue' }, - }; - }, - async recordStop() { - return { intervalId: 'interval-1', durable: 'pg', dedup: false }; - }, - }; - const provider = createFakeProvider(); - const stub = await startAllocation(provider, { - sandboxId, - provider: 'vercel', - configuration: { - provider: 'vercel', - resources: { vcpus: identity === 'invalid-size' ? 3 : 2, memory: 4096 }, - }, - billing: { - sandboxId, - subject: { type: 'user', id: identity === 'wrong-owner' ? 'other' : 'owner-1' }, - actor: { type: 'user', id: 'owner-1' }, - sessionId: 'workspace_11111111-1111-4111-8111-111111111111', - metadata: { origin: 'cloud-agent' }, - enforcementRequested: false, - }, - meter, - preparingRoute: 'workspace_shadow_exception', - admissionError: 'throw-after-open', - }); - if (identity === 'valid') { - await waitFor(() => expect(provider.createCalls).toBe(1)); - const context = await runInDurableObject(stub, (_instance, state) => - getBillingContext(state.storage) - ); - expect(context?.subject.id).toBe('owner-1'); - } else { - await waitFor(async () => expect((await readState(stub)).kind).toBe('stopped')); - expect(provider.createCalls).toBe(0); - const route = await runInDurableObject(stub, async (_instance, state) => { - const db = drizzle(state.storage, { logger: false }); - return db - .select() - .from(routesTable) - .where(eq(routesTable.session_id, 'workspace_shadow_exception')); - }); - expect(route[0]).toMatchObject({ state: 'failed', reason: 'billing_unavailable' }); - } - } - ); }); describe('SandboxControlV2 allocation egress policy', () => { diff --git a/services/cloud-agent-next/test/plugins/binary-as-arraybuffer.ts b/services/cloud-agent-next/test/plugins/binary-as-arraybuffer.ts new file mode 100644 index 00000000000..a3a29b2370e --- /dev/null +++ b/services/cloud-agent-next/test/plugins/binary-as-arraybuffer.ts @@ -0,0 +1,19 @@ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import type { Plugin } from 'vitest/config'; + +// Match Wrangler Data modules in both Node and Workers tests. Read the file +// during transformation so the Worker does not need filesystem access. +export const binaryAsArrayBuffer: Plugin = { + name: 'wrangler-binary-as-arraybuffer', + enforce: 'pre', + resolveId(source, importer) { + if (!source.endsWith('?binary') || !importer) return null; + return resolve(dirname(importer), source.slice(0, -'?binary'.length)) + '?binary'; + }, + load(id) { + if (!id.endsWith('?binary')) return null; + const encoded = readFileSync(id.slice(0, -'?binary'.length)).toString('base64'); + return `export default Uint8Array.from(atob(${JSON.stringify(encoded)}), char => char.charCodeAt(0)).buffer;`; + }, +}; diff --git a/services/cloud-agent-next/test/unit/e2e/client.test.ts b/services/cloud-agent-next/test/unit/e2e/client.test.ts index c95eafea578..9e5e565a121 100644 --- a/services/cloud-agent-next/test/unit/e2e/client.test.ts +++ b/services/cloud-agent-next/test/unit/e2e/client.test.ts @@ -432,7 +432,7 @@ describe('startSession allocation selection', () => { await startSession({ ...config, sandboxAllocation: 'vercel-small' }, { prompt: 'echo:hi' }); expect(bodyOf(fetchMock).runtime).toEqual({ - sandboxAllocation: { provider: { id: 'vercel', account: 'kilo' }, instanceType: 'small' }, + sandboxAllocation: { provider: { id: 'vercel', account: 'byoc' }, instanceType: 'small' }, }); }); @@ -456,7 +456,7 @@ describe('startSession allocation selection', () => { ); expect(bodyOf(fetchMock).sandboxAllocation).toEqual({ - provider: { id: 'vercel', account: 'kilo' }, + provider: { id: 'vercel', account: 'byoc' }, instanceType: 'small', }); }); diff --git a/services/cloud-agent-next/vitest.config.ts b/services/cloud-agent-next/vitest.config.ts index 0043256f228..9c573b39654 100644 --- a/services/cloud-agent-next/vitest.config.ts +++ b/services/cloud-agent-next/vitest.config.ts @@ -1,5 +1,6 @@ import { readFileSync } from 'node:fs'; import { defineConfig, type Plugin } from 'vitest/config'; +import { binaryAsArrayBuffer } from './test/plugins/binary-as-arraybuffer.js'; // Mirrors the wrangler.jsonc Text rule for `**/*.sql`: Durable Object migrations // import migration SQL as modules. `vitest related` analyzes every test file's @@ -17,7 +18,7 @@ const sqlAsText: Plugin = { // Unit tests - run in Node (fast, supports vi.mock and global mocking) export default defineConfig({ - plugins: [sqlAsText], + plugins: [sqlAsText, binaryAsArrayBuffer], test: { name: 'unit', globals: true, diff --git a/services/cloud-agent-next/vitest.workers.config.ts b/services/cloud-agent-next/vitest.workers.config.ts index c661d9847fc..216695a7275 100644 --- a/services/cloud-agent-next/vitest.workers.config.ts +++ b/services/cloud-agent-next/vitest.workers.config.ts @@ -1,11 +1,13 @@ import { createRequire } from 'node:module'; import { cloudflareTest } from '@cloudflare/vitest-pool-workers'; import { defineConfig } from 'vitest/config'; +import { binaryAsArrayBuffer } from './test/plugins/binary-as-arraybuffer.js'; // Integration tests - run in Cloudflare Workers runtime via Miniflare // Use cloudflare:test utilities: env, runInDurableObject, createMessageBatch, etc. export default defineConfig({ plugins: [ + binaryAsArrayBuffer, { name: 'fix-pg-cjs-dependencies', enforce: 'pre', @@ -25,6 +27,13 @@ export default defineConfig({ configPath: './wrangler.test.jsonc', }, miniflare: { + // Lifecycle fixtures own their startup mode. Do not inherit a local + // developer's downloaded release or runtime-free image selection. + bindings: { + RUNTIME_STARTUP_MODE: 'preinstalled', + RUNTIME_RELEASE_DESCRIPTOR: '', + NATIVE_RUNTIME_IMAGE: 'all-in-one', + }, // Faster queue processing in tests queueConsumers: { EXECUTION_QUEUE: { diff --git a/services/cloud-agent-next/worker-configuration.d.ts b/services/cloud-agent-next/worker-configuration.d.ts index 9f5f5cb2633..d1ee6cfb296 100644 --- a/services/cloud-agent-next/worker-configuration.d.ts +++ b/services/cloud-agent-next/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 5b7e20bbc64d17d3119ab9c0b51e431a) +// Generated by Wrangler by running `wrangler types` (hash: a44d9be39ee1b18b414b315992c09c27) // Runtime types generated with workerd@1.20260918.1 2026-06-03 nodejs_compat interface __BaseEnv_Env { SHARED_SANDBOX_OVERRIDES: KVNamespace; @@ -11,38 +11,62 @@ interface __BaseEnv_Env { CLOUD_AGENT_REPORT_QUEUE: Queue; INTERNAL_API_SECRET_PROD: SecretsStoreSecret; EXPERIMENTAL_CLOUDFLARE_CONTAINER_IMAGES: Readonly>>>; - KILOCODE_BACKEND_BASE_URL: "http://localhost:3000" | "https://api.kilo.ai"; - KILO_OPENROUTER_BASE?: "http://localhost:3000/api"; - GITHUB_APP_SLUG: "kiloconnect-development" | "kiloconnect"; - GITHUB_APP_BOT_USER_ID: "242397087" | "240665456"; GITHUB_LITE_APP_SLUG: "" | "kiloconnect-lite"; GITHUB_LITE_APP_BOT_USER_ID: "" | "257753004"; - WORKER_URL: "http://localhost:8794" | "https://cloud-agent-next.kilosessions.ai"; SANDBOX_TRANSPORT?: "rpc"; - CLI_TIMEOUT_SECONDS: "900"; - REAPER_INTERVAL_MS: "300000"; R2_ATTACHMENTS_BUCKET: "cloud-agent-attachments-dev" | "cloud-agent-attachments"; - WS_ALLOWED_ORIGINS: "http://localhost:3000,http://host.docker.internal:3000" | "https://app.kilo.ai,https://api.kilo.ai"; - KILO_SESSION_INGEST_URL: "http://localhost:8800" | "https://ingest.kilosessions.ai"; - PER_SESSION_SANDBOX_ORG_IDS?: "*"; CREDENTIAL_CONTAINMENT_ENABLED: "false" | "true"; CONTAINER_BILLING_HEARTBEAT_SECONDS: "60" | "300"; CLOUD_AGENT_CONTAINER_BILLING_ENABLED: "false" | "true"; CLOUD_AGENT_CONTAINER_BILLING_USER_IDS: "" | "f1a848ca-bade-48d8-a5ad-1042d08651e6"; CLOUD_AGENT_CONTAINER_BILLING_ORG_IDS: "" | "9d278969-5453-4ae3-a51f-a8d2274a7b56"; - CODE_REVIEW_CONTAINERS_ORG_IDS: "" | "9d278969-5453-4ae3-a51f-a8d2274a7b56"; + CODE_REVIEW_CONTAINERS_ORG_IDS: "" | "*"; RUNTIME_ISOLATION_ENABLED: "true"; SANDBOX_SELECTION_IDS: "*" | "9d278969-5453-4ae3-a51f-a8d2274a7b56,30f1620a-4aad-4456-bf4d-550f335e6f55"; CLOUDFLARE_CONTAINERS_ORG_IDS: "*"; PREVIEW_ENABLED_IDS: "*"; - VERCEL_SANDBOX_ORG_IDS: ""; - VERCEL_PROJECT_ID: ""; - VERCEL_TEAM_ID: ""; - VERCEL_SANDBOX_SNAPSHOT_ID: ""; - VERCEL_SANDBOX_RUNTIME_BUILD_ID: ""; - VERCEL_SANDBOX_RUNTIME: "node24"; - VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: ""; - VERCEL_SANDBOX_EXTEND_DURATION_MS: ""; + BYOC_VERCEL_IDS: "" | "9d278969-5453-4ae3-a51f-a8d2274a7b56"; + VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: "300000"; + VERCEL_SANDBOX_EXTEND_DURATION_MS: "600000"; + NEXTAUTH_SECRET: string; + KILO_SESSION_INGEST_URL: string; + WORKER_URL: string; + WS_ALLOWED_ORIGINS: string; + KILOCODE_BACKEND_BASE_URL: string; + KILO_OPENROUTER_BASE: string; + INTERNAL_API_SECRET: string; + R2_ENDPOINT: string; + R2_ATTACHMENTS_READONLY_ACCESS_KEY_ID: string; + R2_ATTACHMENTS_READONLY_SECRET_ACCESS_KEY: string; + AGENT_ENV_VARS_PRIVATE_KEY: string; + GITHUB_APP_ID: string; + GITHUB_APP_PRIVATE_KEY: string; + CLI_TIMEOUT_SECONDS: string; + REAPER_INTERVAL_MS: string; + STALE_THRESHOLD_MS: string; + PENDING_START_TIMEOUT_MS: string; + GITHUB_APP_SLUG: string; + GITHUB_APP_BOT_USER_ID: string; + GITHUB_LITE_APP_ID: string; + GITHUB_LITE_APP_PRIVATE_KEY: string; + KILOCODE_SANDBOX_BACKEND_BASE_URL: string; + VERCEL_TOKEN: string; + VERCEL_TEAM_ID: string; + VERCEL_PROJECT_ID: string; + VERCEL_SANDBOX_SNAPSHOT_ID: string; + VERCEL_SANDBOX_RUNTIME_BUILD_ID: string; + VERCEL_SANDBOX_RUNTIME: string; + CONTROL_PLANE_IDS: string; + WORKTREE_CREATION_ENABLED_IDS: string; + PER_SESSION_SANDBOX_ORG_IDS: string; + VERCEL_SANDBOX_ORG_IDS: string; + KILOCODE_TOKEN_CONTAINMENT_ORG_IDS: string; + GITHUB_TOKEN_CONTAINMENT_ORG_IDS: string; + R2_ACCESS_KEY_ID: string; + R2_SECRET_ACCESS_KEY: string; + LOG_REJECTED_KILO_URLS: string; + CLOUD_AGENT_MCP_URL_OVERRIDE: string; + PREVIEW_HOST_SUFFIX: string; Sandbox: DurableObjectNamespace; SandboxSmall: DurableObjectNamespace; SandboxDIND: DurableObjectNamespace; @@ -55,6 +79,7 @@ interface __BaseEnv_Env { SANDBOX_CONTROL: DurableObjectNamespace; SANDBOX_SESSION: DurableObjectNamespace; SANDBOX_CONTAINERS: DurableObjectNamespace; + VERCEL_SNAPSHOT_BUILD: DurableObjectNamespace; SESSION_INGEST: Service /* entrypoint SessionIngestRPC from session-ingest */; GIT_TOKEN_SERVICE: Service /* entrypoint GitTokenRPCEntrypoint from git-token-service-dev */ | Service /* entrypoint GitTokenRPCEntrypoint from git-token-service */; NOTIFICATIONS: Service /* entrypoint NotificationsService from notifications */; @@ -62,13 +87,12 @@ interface __BaseEnv_Env { TOOL_CGROUP_RESERVE_MB?: "2048"; TOOL_CGROUP_CPU_WEIGHT?: "50"; TOOL_CGROUP_SERVER_CPU_WEIGHT?: "75"; - CONTAINER_REPO_SNAPSHOT_IDS?: "2bbc9955-5dc2-428c-a353-be59d89a8130"; - PREVIEW_HOST_SUFFIX?: "cloud-agent.kiloapps.ai"; + CONTAINER_REPO_SNAPSHOT_IDS?: ""; } declare namespace Cloudflare { interface GlobalProps { mainModule: typeof import("./src/index"); - durableNamespaces: "Sandbox" | "SandboxSmall" | "SandboxDIND" | "UserKiloFacade" | "SandboxCodeReview" | "SandboxContainment" | "SandboxSmallContainment" | "SandboxCodeReviewContainment" | "StreamTicketNonceDO" | "SandboxControl" | "SandboxSession" | "SandboxContainers"; + durableNamespaces: "Sandbox" | "CloudAgentSession" | "SandboxSmall" | "SandboxDIND" | "UserKiloFacade" | "SandboxCodeReview" | "SandboxContainment" | "SandboxSmallContainment" | "SandboxCodeReviewContainment" | "StreamTicketNonceDO" | "SandboxControl" | "SandboxSession" | "SandboxContainers" | "VercelSnapshotBuild"; } interface DevEnv { SHARED_SANDBOX_OVERRIDES: KVNamespace; @@ -80,20 +104,10 @@ declare namespace Cloudflare { CLOUD_AGENT_REPORT_QUEUE: Queue; INTERNAL_API_SECRET_PROD: SecretsStoreSecret; EXPERIMENTAL_CLOUDFLARE_CONTAINER_IMAGES: Readonly>>>; - KILOCODE_BACKEND_BASE_URL: "http://localhost:3000"; - KILO_OPENROUTER_BASE: "http://localhost:3000/api"; - GITHUB_APP_SLUG: "kiloconnect-development"; - GITHUB_APP_BOT_USER_ID: "242397087"; GITHUB_LITE_APP_SLUG: ""; GITHUB_LITE_APP_BOT_USER_ID: ""; - WORKER_URL: "http://localhost:8794"; SANDBOX_TRANSPORT: "rpc"; - CLI_TIMEOUT_SECONDS: "900"; - REAPER_INTERVAL_MS: "300000"; R2_ATTACHMENTS_BUCKET: "cloud-agent-attachments-dev"; - WS_ALLOWED_ORIGINS: "http://localhost:3000,http://host.docker.internal:3000"; - KILO_SESSION_INGEST_URL: "http://localhost:8800"; - PER_SESSION_SANDBOX_ORG_IDS: "*"; CREDENTIAL_CONTAINMENT_ENABLED: "false"; CONTAINER_BILLING_HEARTBEAT_SECONDS: "60"; CLOUD_AGENT_CONTAINER_BILLING_ENABLED: "false"; @@ -104,14 +118,48 @@ declare namespace Cloudflare { SANDBOX_SELECTION_IDS: "*"; CLOUDFLARE_CONTAINERS_ORG_IDS: "*"; PREVIEW_ENABLED_IDS: "*"; - VERCEL_SANDBOX_ORG_IDS: ""; - VERCEL_PROJECT_ID: ""; - VERCEL_TEAM_ID: ""; - VERCEL_SANDBOX_SNAPSHOT_ID: ""; - VERCEL_SANDBOX_RUNTIME_BUILD_ID: ""; - VERCEL_SANDBOX_RUNTIME: "node24"; - VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: ""; - VERCEL_SANDBOX_EXTEND_DURATION_MS: ""; + BYOC_VERCEL_IDS: ""; + VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: "300000"; + VERCEL_SANDBOX_EXTEND_DURATION_MS: "600000"; + NEXTAUTH_SECRET: string; + KILO_SESSION_INGEST_URL: string; + WORKER_URL: string; + WS_ALLOWED_ORIGINS: string; + KILOCODE_BACKEND_BASE_URL: string; + KILO_OPENROUTER_BASE: string; + INTERNAL_API_SECRET: string; + R2_ENDPOINT: string; + R2_ATTACHMENTS_READONLY_ACCESS_KEY_ID: string; + R2_ATTACHMENTS_READONLY_SECRET_ACCESS_KEY: string; + AGENT_ENV_VARS_PRIVATE_KEY: string; + GITHUB_APP_ID: string; + GITHUB_APP_PRIVATE_KEY: string; + CLI_TIMEOUT_SECONDS: string; + REAPER_INTERVAL_MS: string; + STALE_THRESHOLD_MS: string; + PENDING_START_TIMEOUT_MS: string; + GITHUB_APP_SLUG: string; + GITHUB_APP_BOT_USER_ID: string; + GITHUB_LITE_APP_ID: string; + GITHUB_LITE_APP_PRIVATE_KEY: string; + KILOCODE_SANDBOX_BACKEND_BASE_URL: string; + VERCEL_TOKEN: string; + VERCEL_TEAM_ID: string; + VERCEL_PROJECT_ID: string; + VERCEL_SANDBOX_SNAPSHOT_ID: string; + VERCEL_SANDBOX_RUNTIME_BUILD_ID: string; + VERCEL_SANDBOX_RUNTIME: string; + CONTROL_PLANE_IDS: string; + WORKTREE_CREATION_ENABLED_IDS: string; + PER_SESSION_SANDBOX_ORG_IDS: string; + VERCEL_SANDBOX_ORG_IDS: string; + KILOCODE_TOKEN_CONTAINMENT_ORG_IDS: string; + GITHUB_TOKEN_CONTAINMENT_ORG_IDS: string; + R2_ACCESS_KEY_ID: string; + R2_SECRET_ACCESS_KEY: string; + LOG_REJECTED_KILO_URLS: string; + CLOUD_AGENT_MCP_URL_OVERRIDE: string; + PREVIEW_HOST_SUFFIX: string; Sandbox: DurableObjectNamespace; SandboxSmall: DurableObjectNamespace; SandboxDIND: DurableObjectNamespace; @@ -124,6 +172,7 @@ declare namespace Cloudflare { SANDBOX_CONTROL: DurableObjectNamespace; SANDBOX_SESSION: DurableObjectNamespace; SANDBOX_CONTAINERS: DurableObjectNamespace; + VERCEL_SNAPSHOT_BUILD: DurableObjectNamespace; SESSION_INGEST: Service /* entrypoint SessionIngestRPC from session-ingest */; GIT_TOKEN_SERVICE: Service /* entrypoint GitTokenRPCEntrypoint from git-token-service-dev */; NOTIFICATIONS: Service /* entrypoint NotificationsService from notifications */; @@ -136,12 +185,20 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } declare module "*.sql" { const value: string; export default value; } +declare module "*.js?binary" { + const value: ArrayBuffer; + export default value; + } +declare module "*.sh?binary" { + const value: ArrayBuffer; + export default value; + } // Begin runtime types /*! ***************************************************************************** Copyright (c) Cloudflare. All rights reserved. diff --git a/services/cloud-agent-next/wrangler.jsonc b/services/cloud-agent-next/wrangler.jsonc index 230b7c1a2f8..530befeafbd 100644 --- a/services/cloud-agent-next/wrangler.jsonc +++ b/services/cloud-agent-next/wrangler.jsonc @@ -10,7 +10,16 @@ "compatibility_date": "2026-06-03", "compatibility_flags": ["nodejs_compat"], "preview_urls": false, - "rules": [{ "type": "Text", "globs": ["**/*.sql"], "fallthrough": true }], + "rules": [ + { "type": "Text", "globs": ["**/*.sql"], "fallthrough": true }, + { + "type": "Data", + "globs": [ + "**/wrapper/dist/control-plane-wrapper.js?binary", + "**/wrapper/control-plane-supervisor.sh?binary", + ], + }, + ], "workers_dev": true, "dev": { "port": 8794, @@ -78,14 +87,15 @@ "CLOUDFLARE_CONTAINERS_ORG_IDS": "*", "PREVIEW_HOST_SUFFIX": "cloud-agent.kiloapps.ai", "PREVIEW_ENABLED_IDS": "*", + "BYOC_VERCEL_IDS": "9d278969-5453-4ae3-a51f-a8d2274a7b56", "VERCEL_SANDBOX_ORG_IDS": "", "VERCEL_PROJECT_ID": "", "VERCEL_TEAM_ID": "", "VERCEL_SANDBOX_SNAPSHOT_ID": "", "VERCEL_SANDBOX_RUNTIME_BUILD_ID": "", "VERCEL_SANDBOX_RUNTIME": "node24", - "VERCEL_SANDBOX_INITIAL_TIMEOUT_MS": "", - "VERCEL_SANDBOX_EXTEND_DURATION_MS": "", + "VERCEL_SANDBOX_INITIAL_TIMEOUT_MS": "300000", + "VERCEL_SANDBOX_EXTEND_DURATION_MS": "600000", "CODE_REVIEW_CONTAINERS_ORG_IDS": "*", }, "placement": { "mode": "smart" }, @@ -345,6 +355,10 @@ "class_name": "SandboxContainers", "name": "SANDBOX_CONTAINERS", }, + { + "class_name": "VercelSnapshotBuild", + "name": "VERCEL_SNAPSHOT_BUILD", + }, ], }, "migrations": [ @@ -396,6 +410,10 @@ "new_sqlite_classes": ["SandboxContainers"], "tag": "v11", }, + { + "new_sqlite_classes": ["VercelSnapshotBuild"], + "tag": "v12", + }, ], /** * Hyperdrive Bindings @@ -499,14 +517,10 @@ "SANDBOX_SELECTION_IDS": "*", "CLOUDFLARE_CONTAINERS_ORG_IDS": "*", "PREVIEW_ENABLED_IDS": "*", - "VERCEL_SANDBOX_ORG_IDS": "", - "VERCEL_PROJECT_ID": "", - "VERCEL_TEAM_ID": "", - "VERCEL_SANDBOX_SNAPSHOT_ID": "", - "VERCEL_SANDBOX_RUNTIME_BUILD_ID": "", + "BYOC_VERCEL_IDS": "", "VERCEL_SANDBOX_RUNTIME": "node24", - "VERCEL_SANDBOX_INITIAL_TIMEOUT_MS": "", - "VERCEL_SANDBOX_EXTEND_DURATION_MS": "", + "VERCEL_SANDBOX_INITIAL_TIMEOUT_MS": "300000", + "VERCEL_SANDBOX_EXTEND_DURATION_MS": "600000", }, "services": [ { @@ -691,6 +705,10 @@ "class_name": "SandboxContainers", "name": "SANDBOX_CONTAINERS", }, + { + "class_name": "VercelSnapshotBuild", + "name": "VERCEL_SNAPSHOT_BUILD", + }, ], }, "queues": { diff --git a/services/cloud-agent-next/wrapper/src/control/terminal-runtime.ts b/services/cloud-agent-next/wrapper/src/control/terminal-runtime.ts index 8f3ddde7440..93213c0e612 100644 --- a/services/cloud-agent-next/wrapper/src/control/terminal-runtime.ts +++ b/services/cloud-agent-next/wrapper/src/control/terminal-runtime.ts @@ -102,6 +102,22 @@ function sameSession(left: SessionRequestIdentity, right: SessionRequestIdentity ); } +const SESSION_UNAUTHORIZED = { + shutDown: 'Session runtime is shut down', + kiloRuntimeMissing: 'Session kilo runtime missing', + directoryMapMismatch: 'Session directory map mismatch', + rootMapMismatch: 'Session root map mismatch', + kiloSessionMismatch: 'Session kilo session mismatch', + attachedDirectoryMismatch: 'Session attached directory mismatch', + staleKiloRuntime: 'Session stale kilo runtime', + duplicateKiloSession: 'Session duplicate kilo session', + wrapperMismatch: 'Session wrapper mismatch', +} as const; + +function rejectUnauthorized(reason: keyof typeof SESSION_UNAUTHORIZED): never { + throw new ControlTerminalRuntimeError('unauthorized', SESSION_UNAUTHORIZED[reason], false); +} + function safeKiloFailure(error: unknown, message: string): ControlTerminalRuntimeError { if (error instanceof ControlTerminalRuntimeError) return error; return new ControlTerminalRuntimeError('not_ready', message, isKiloServerUnreachableError(error)); @@ -179,18 +195,14 @@ export function createControlTerminalRuntime(options: { if (!attached || shutDown || recoveringSessions.has(identity.sessionId)) { throw new ControlTerminalRuntimeError('not_ready', 'Terminal session is not attached', true); } - if ( - attached.wrapperInstanceId !== wrapperInstanceId || - !sameSession(attached, identity) || - directoryForSession(identity.kiloSessionId) !== identity.directory || - rootForSession(identity.kiloSessionId) !== identity.kiloSessionId - ) { - throw new ControlTerminalRuntimeError( - 'unauthorized', - 'Terminal session ownership mismatch', - false - ); - } + if (attached.wrapperInstanceId !== wrapperInstanceId) rejectUnauthorized('wrapperMismatch'); + if (attached.kiloSessionId !== identity.kiloSessionId) + rejectUnauthorized('kiloSessionMismatch'); + if (attached.directory !== identity.directory) rejectUnauthorized('attachedDirectoryMismatch'); + if (directoryForSession(identity.kiloSessionId) !== identity.directory) + rejectUnauthorized('directoryMapMismatch'); + if (rootForSession(identity.kiloSessionId) !== identity.kiloSessionId) + rejectUnauthorized('rootMapMismatch'); const runtime = options.getKiloRuntime(identity); if ( !runtime || @@ -397,13 +409,9 @@ export function createControlTerminalRuntime(options: { async function detachSession(identity: SessionRequestIdentity): Promise { const attached = attachedSessions.get(identity.sessionId); if (!attached) return; - if (!sameSession(attached, identity)) { - throw new ControlTerminalRuntimeError( - 'unauthorized', - 'Terminal session ownership mismatch', - false - ); - } + if (attached.kiloSessionId !== identity.kiloSessionId) + rejectUnauthorized('kiloSessionMismatch'); + if (attached.directory !== identity.directory) rejectUnauthorized('attachedDirectoryMismatch'); attachedSessions.delete(identity.sessionId); @@ -467,43 +475,25 @@ export function createControlTerminalRuntime(options: { return { rememberAttachedSession(identity) { const kiloRuntime = options.getKiloRuntime(identity); - if (shutDown || !kiloRuntime) { - throw new ControlTerminalRuntimeError( - 'unauthorized', - 'Terminal session runtime unavailable', - false - ); - } + if (shutDown) rejectUnauthorized('shutDown'); + if (!kiloRuntime) rejectUnauthorized('kiloRuntimeMissing'); if ( kiloRuntime.isolation === 'per-session' && (!kiloRuntime.identity || !sameSession(kiloRuntime.identity, identity)) ) { - throw new ControlTerminalRuntimeError( - 'unauthorized', - 'Terminal session ownership mismatch', - false - ); - } - if ( - directoryForSession(identity.kiloSessionId) !== identity.directory || - rootForSession(identity.kiloSessionId) !== identity.kiloSessionId - ) { - throw new ControlTerminalRuntimeError( - 'unauthorized', - 'Terminal session belongs to another session', - false - ); + rejectUnauthorized('kiloSessionMismatch'); } + if (directoryForSession(identity.kiloSessionId) !== identity.directory) + rejectUnauthorized('directoryMapMismatch'); + if (rootForSession(identity.kiloSessionId) !== identity.kiloSessionId) + rejectUnauthorized('rootMapMismatch'); const existing = attachedSessions.get(identity.sessionId); if (existing) { - if (!sameSession(existing, identity)) { - throw new ControlTerminalRuntimeError( - 'unauthorized', - 'Terminal session belongs to another session', - false - ); - } + if (existing.kiloSessionId !== identity.kiloSessionId) + rejectUnauthorized('kiloSessionMismatch'); + if (existing.directory !== identity.directory) + rejectUnauthorized('attachedDirectoryMismatch'); if (existing.wrapperInstanceId !== wrapperInstanceId) { existing.wrapperInstanceId = wrapperInstanceId; } @@ -511,13 +501,8 @@ export function createControlTerminalRuntime(options: { } for (const attached of attachedSessions.values()) { - if (attached.kiloSessionId === identity.kiloSessionId) { - throw new ControlTerminalRuntimeError( - 'unauthorized', - 'Terminal session belongs to another session', - false - ); - } + if (attached.kiloSessionId === identity.kiloSessionId) + rejectUnauthorized('duplicateKiloSession'); } attachedSessions.set(identity.sessionId, { ...identity, wrapperInstanceId }); diff --git a/services/kilo-mcp/catalog.json b/services/kilo-mcp/catalog.json index 1fc00a2e8c9..c8cae15a1e6 100644 --- a/services/kilo-mcp/catalog.json +++ b/services/kilo-mcp/catalog.json @@ -6103,10 +6103,7 @@ }, "account": { "type": "string", - "enum": [ - "kilo", - "byoc" - ] + "const": "byoc" } }, "required": [ @@ -6820,10 +6817,7 @@ }, "account": { "type": "string", - "enum": [ - "kilo", - "byoc" - ] + "const": "byoc" } }, "required": [ @@ -17046,10 +17040,7 @@ }, "account": { "type": "string", - "enum": [ - "kilo", - "byoc" - ] + "const": "byoc" } }, "required": [ @@ -17769,10 +17760,7 @@ }, "account": { "type": "string", - "enum": [ - "kilo", - "byoc" - ] + "const": "byoc" } }, "required": [ @@ -27642,6 +27630,58 @@ ], "searchBlob": "organizations.usageStats Get an organization's usage totals (cost, requests, tokens) over the last 30 days. organizations usagestats organizationid organizationId" }, + "organizations.vercelCompute.getEnrollment": { + "path": "organizations.vercelCompute.getEnrollment", + "kind": "query", + "summary": "Get the organization's BYOC Vercel compute enrollment state, including whether the organization is enrolled.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" + } + }, + "required": [ + "organizationId" + ] + }, + "tags": [ + "organizations", + "vercelcompute", + "getenrollment", + "organizationid" + ], + "searchBlob": "organizations.vercelCompute.getEnrollment Get the organization's BYOC Vercel compute enrollment state, including whether the organization is enrolled. organizations vercelcompute getenrollment organizationid organizationId" + }, + "organizations.vercelCompute.getStatus": { + "path": "organizations.vercelCompute.getStatus", + "kind": "query", + "summary": "Get the organization's BYOC Vercel compute status: setup progress, setup and upgrade errors, runtime build and snapshot identity, and whether a newer runtime is available.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" + } + }, + "required": [ + "organizationId" + ] + }, + "tags": [ + "organizations", + "vercelcompute", + "getstatus", + "organizationid" + ], + "searchBlob": "organizations.vercelCompute.getStatus Get the organization's BYOC Vercel compute status: setup progress, setup and upgrade errors, runtime build and snapshot identity, and whether a newer runtime is available. organizations vercelcompute getstatus organizationid organizationId" + }, "organizations.withMembers": { "path": "organizations.withMembers", "kind": "query", @@ -34788,6 +34828,28 @@ ], "searchBlob": "userFeedback.create Submit user feedback or a bug report to the Kilo team along with context about the app area and source it came from. userfeedback create feedback_text feedback_for feedback_batch source context_json feedback_text feedback_for feedback_batch source context_json" }, + "vercelCompute.getEnrollment": { + "path": "vercelCompute.getEnrollment", + "kind": "query", + "summary": "Check whether an organization is enrolled in customer-paid Vercel compute (bring your own Vercel account) and can use the integration.", + "inputSchema": {}, + "tags": [ + "vercelcompute", + "getenrollment" + ], + "searchBlob": "vercelCompute.getEnrollment Check whether an organization is enrolled in customer-paid Vercel compute (bring your own Vercel account) and can use the integration. vercelcompute getenrollment" + }, + "vercelCompute.getStatus": { + "path": "vercelCompute.getStatus", + "kind": "query", + "summary": "Get the current Vercel compute setup status for an organization, including setup progress, errors, connected team and project, runtime version, and whether a runtime upgrade is available.", + "inputSchema": {}, + "tags": [ + "vercelcompute", + "getstatus" + ], + "searchBlob": "vercelCompute.getStatus Get the current Vercel compute setup status for an organization, including setup progress, errors, connected team and project, runtime version, and whether a runtime upgrade is available. vercelcompute getstatus" + }, "webhookTriggers.capabilities": { "path": "webhookTriggers.capabilities", "kind": "query",