diff --git a/.specs/cloud-agent-session.md b/.specs/cloud-agent-session.md index d3ed75b2dd8..52d1927fec0 100644 --- a/.specs/cloud-agent-session.md +++ b/.specs/cloud-agent-session.md @@ -36,6 +36,9 @@ they appear in all capitals, as shown here. - **Repository environment**: an optional saved setup for one repository -- setup commands, plaintext variables, secrets, and a sandbox size -- distinct from the sandbox Environment. An owner has at most one per repository. +- **Environment snapshot**: a prepared copy of a repository environment's + default branch after its saved setup, built from the base image on request + or for a session, that later sessions of that environment start from. - **Worktree**: a group of independent browser chats that share the same checked-out repository files while their environment exists. - **Preparation**: visible setup of that environment -- clone, branch, setup @@ -112,7 +115,7 @@ repository. setup command's output. 2. Steps the user MUST be able to see when they run: cloning the repository, using a prepared repository, checking out the branch, restoring a previous - workspace, running setup commands, saving the repository for faster starts. + workspace, running setup commands. 3. Preparation that only acquired and booted an environment -- warm reuse, no real provisioning -- MUST NOT leave a completed preparation row. 4. Running and failed preparation MUST always be visible. Failed preparation, @@ -133,6 +136,38 @@ repository. transport permits sending. A later submission MUST use fresh message and preparation-attempt identities. +### Environment snapshots + +1. A session MUST NOT save its workspace as a snapshot. Only an environment + build MAY create an environment snapshot. +2. A session that uses a repository environment MAY start from that + environment's published snapshot when the owner, repository, sandbox image + and runtime match and the snapshot revision is not newer than the + session's configuration revision. Otherwise, and when the lookup is unavailable, it MUST start from + the base image. The lookup MUST NOT delay the start by more than 15 seconds + and MUST NOT wait for a build. +3. A session that starts from a snapshot MUST still run setup with the + configuration admitted for that session. Cached files MAY retain earlier + values, including secrets. +4. When a session starts from an older snapshot revision, preparation MUST + show the snapshot revision, the session's revision, and the actual + replacement build status, including a skipped build or backoff. +5. If a snapshot start fails, the session MUST start once from the base image + within the same preparation deadline. +6. A snapshot MUST NOT contain git or build credentials, reflogs, or a Kilo + home. +7. Organization managers and personal owners MAY build, refresh, or cancel a + build and read its redacted log. Members MAY read the build status only. A + build MUST use saved settings only; the UI MUST require saving first. +8. A build whose revision changes before publication MUST NOT publish. A + failed refresh MUST NOT remove the previous snapshot. Deleting the + environment MUST cancel its builds. +9. Automatic builds started by sessions MUST back off after repeated failures + of one revision. A manual build MUST stay available during backoff. +10. Environment, profile, and launch screens MUST show a non-blocking notice + that sessions can reuse a snapshot and cached files may retain earlier + values. The notice MUST NOT block a save or a launch or show a value. + ### Turn 1. The agent MUST stream text, reasoning, and tool calls in the same chat. @@ -495,6 +530,12 @@ The following use SHOULD and are not enforced today: - Profile setup commands do not run while an environment applies, so a setup moved into the environment never runs twice. - Saving a sandbox size that the owner cannot use for the repository fails. +### 2026-10-16 -- Environment snapshots + +- Added environment snapshots: built from saved environment settings on the + base image, consumed by sessions of the same environment and an equal or + newer revision, with a bounded lookup and one fresh fallback. +- Removed session capture and the "saving the repository" preparation step. ### 2026-10-07 -- Repository environments diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md index a2bcabad05d..8634ef61593 100644 --- a/ENVIRONMENT.md +++ b/ENVIRONMENT.md @@ -473,6 +473,7 @@ The key is team-scoped for all topics and valid in both the sandbox and producti - `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] +- `ENVIRONMENT_SNAPSHOT_ORG_IDS` - Comma-separated organization IDs allowed to build and restore environment snapshots. Empty or unset disables snapshots. Personal accounts and `*` are excluded. Native-container enrollment is also required. Removing an organization stops its active builds and prevents restore of its existing snapshots. [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] - `HOME` - Reserved in `RESERVED_ENV_VARS` for cloud-agent-next session home management. [SYSTEM] diff --git a/apps/web/src/components/cloud-agent-next/EnvironmentBuildPanel.tsx b/apps/web/src/components/cloud-agent-next/EnvironmentBuildPanel.tsx new file mode 100644 index 00000000000..27053cb37bc --- /dev/null +++ b/apps/web/src/components/cloud-agent-next/EnvironmentBuildPanel.tsx @@ -0,0 +1,431 @@ +'use client'; + +import { useState } from 'react'; +import { toast } from 'sonner'; +import { Loader2, RefreshCw, Square, Hammer } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { environmentSnapshotName } from '../../../../../services/cloud-agent-next/src/shared/environment-snapshot-name'; +import { + useEnvironmentBuildLogs, + useEnvironmentBuildMutations, + useEnvironmentBuildStatus, +} from '@/hooks/useCloudAgentEnvironments'; +import { + buildAction, + buildStageLabel, + failureText, + formatAge, + formatDuration, + isBuildActive, + backoffText, + setupBudgetWarning, + stalenessText, + type BuildAction, + type EnvironmentBuildRequestResult, + type EnvironmentBuildStatus, + type EnvironmentBuildSummary, +} from './environment-build-display'; +import { EnvironmentSnapshotNotice } from './EnvironmentSnapshotNotice'; + +type EnvironmentBuildPanelProps = { + organizationId?: string; + environmentId: string; + canManage: boolean; + hasUnsavedChanges: boolean; +}; + +/** + * Keeps the published snapshot separate from replacement build activity. + * Build logs load only when their dialog is open. + */ +export function EnvironmentBuildPanel({ + organizationId, + environmentId, + canManage, + hasUnsavedChanges, +}: EnvironmentBuildPanelProps) { + const statusQuery = useEnvironmentBuildStatus(environmentId, { organizationId, poll: true }); + const status = statusQuery.data; + const action = buildAction(status, { canManage, hasUnsavedChanges }); + + return ( +
+
+
+

+ Current snapshot +

+
+
+ {statusQuery.isLoading ? ( + + ) : !status ? ( +
+

Could not load the snapshot status.

+ +
+ ) : ( + + )} + {status?.supported && ( +
+
+

Snapshot build

+ +
+ + {action.kind === 'none' && !statusQuery.isError && ( +

{action.reason}

+ )} + {(action.kind === 'build' || action.kind === 'refresh') && action.disabledReason && ( +

{action.disabledReason}

+ )} +
+ )} + {status?.published && } +
+ ); +} + +function refusalMessage( + result: Exclude +): string { + if (result.status === 'rejected') + return failureText(result.reason) ?? 'The build could not start.'; + if (result.reason === 'revision_changed') + return 'The configuration changed. Reload and try again.'; + if (result.reason === 'cleanup_pending') return 'The previous builder is still stopping.'; + return 'Another build is running. Cancel it to build the current configuration.'; +} + +/** + * Starts or cancels a build and reports the outcome as a toast. The build + * detail and the environment menu share it, so both apply the same rules. + */ +export function useEnvironmentBuildControl(input: { + organizationId?: string; + environmentId: string; + status: EnvironmentBuildStatus | undefined; +}) { + const { organizationId, environmentId, status } = input; + const { requestBuild, isRequesting, cancelBuild } = useEnvironmentBuildMutations(); + + const start = async () => { + if (!status) return; + try { + const result = await requestBuild({ + organizationId, + environmentId, + expectedRevision: status.configurationRevision, + }); + if (result.status !== 'accepted') { + toast.error(refusalMessage(result)); + return; + } + toast.success(result.created ? 'Snapshot build started' : 'A build is already running'); + } catch { + toast.error('Could not start the build. Try again.'); + } + }; + + const cancel = (buildId: string) => + cancelBuild.mutate( + { organizationId, environmentId, buildId }, + { onError: () => toast.error('Could not cancel the build') } + ); + + return { start, cancel, isPending: isRequesting || cancelBuild.isPending }; +} + +function BuildActionButton({ + action, + organizationId, + environmentId, + status, +}: { + action: BuildAction; + organizationId?: string; + environmentId: string; + status: EnvironmentBuildStatus | undefined; +}) { + const control = useEnvironmentBuildControl({ organizationId, environmentId, status }); + + if (action.kind === 'none' || !status) return null; + if (action.kind === 'cancel') { + return ( + + ); + } + return ( + + ); +} + +function BuildStatusDetail({ + status, + organizationId, + environmentId, + canManage, +}: { + status: EnvironmentBuildStatus; + organizationId?: string; + environmentId: string; + canManage: boolean; +}) { + if (!status.supported) { + return ( +

+ Snapshot builds are unavailable: {status.unsupportedReason ?? 'not supported'}. Sessions run + setup from the base image. +

+ ); + } + const published = status.published; + const staleness = stalenessText(status); + const budget = setupBudgetWarning(status); + return ( +
+ {published ? ( +
+
+

+ + {published.stale !== null && Stale} +

+

+ {published.sourceBranch} ·{' '} + {published.sourceCommit.slice(0, 8)} +

+
+ + + + + +

Snapshot details

+
+
Created
+
{formatAge(published.capturedAt, Date.now())}
+
Clone and setup
+
{formatDuration(published.setupDurationMs)}
+
+

+ Clone and setup excludes sandbox startup and snapshot capture. +

+ {canManage && ( + + )} +
+
+
+ ) : ( +

Not built yet.

+ )} + {staleness &&

{staleness}

} + {budget &&

{budget}

} +
+ ); +} + +function BuildActivity({ + status, + organizationId, + environmentId, + canManage, +}: { + status: EnvironmentBuildStatus; + organizationId?: string; + environmentId: string; + canManage: boolean; +}) { + const active = status.activeBuild; + const latest = status.latestBuild; + const build = active ?? latest; + return ( +
+ {active ? ( + + ) : latest ? ( +

+ {latest.state === 'failed' + ? `Build failed: ${failureText(latest.failureReason) ?? ''}${latest.failureDetail ? ` (${latest.failureDetail})` : ''}` + : latest.state === 'succeeded' + ? 'Build succeeded.' + : latest.state === 'canceled' + ? 'Build canceled.' + : latest.state === 'obsolete' + ? `Build superseded: ${failureText(latest.failureReason) ?? 'environment settings changed.'}` + : buildStageLabel(latest)} +

+ ) : ( +

+ Build a snapshot to prepare this environment for sessions. +

+ )} + {active && status.published && active.buildId !== status.published.buildId && ( +

+ The current snapshot stays available while its replacement builds. +

+ )} + {!active && latest?.state === 'failed' && status.published && ( +

The current snapshot is still available.

+ )} + {status.automaticBackoff && ( +

{backoffText(status.automaticBackoff)}

+ )} + {canManage && build && ( + + )} +
+ ); +} + +function ActiveBuildLine({ build }: { build: EnvironmentBuildSummary }) { + return ( +

+ + + {buildStageLabel(build)} ·{' '} + {build.trigger === 'session' ? 'started by a session' : 'started manually'} + +

+ ); +} + +function BuildLog({ + organizationId, + environmentId, + buildId, + live, + label = 'View build logs', +}: { + organizationId?: string; + environmentId: string; + buildId: string; + live: boolean; + label?: string; +}) { + const [open, setOpen] = useState(false); + const logs = useEnvironmentBuildLogs(environmentId, buildId, { + organizationId, + enabled: open, + live: open && live, + }); + return ( + + + + + + + Snapshot build logs + + {live ? 'Live output from this build.' : 'Output from this build.'} + + + {logs.isLoading ? ( + + ) : logs.isError && !logs.data ? ( +
+

Could not load the build log.

+ +
+ ) : ( + <> + {(logs.data?.lines[0]?.seq ?? 0) > 1 && ( +

+ Showing the latest 500 retained log lines. +

+ )} + {logs.data?.truncated && ( +

Later output was truncated.

+ )} +
+              {(logs.data?.lines ?? []).map(line => line.text).join('\n') || 'No output yet.'}
+            
+ + )} +
+
+ ); +} diff --git a/apps/web/src/components/cloud-agent-next/EnvironmentDialog.tsx b/apps/web/src/components/cloud-agent-next/EnvironmentDialog.tsx index f135c08cd4e..6a082a108ab 100644 --- a/apps/web/src/components/cloud-agent-next/EnvironmentDialog.tsx +++ b/apps/web/src/components/cloud-agent-next/EnvironmentDialog.tsx @@ -26,7 +26,12 @@ import { type EnvironmentResponse, } from '@/hooks/useCloudAgentEnvironments'; import { useEnvironmentProfiles } from '@/hooks/useEnvironmentProfiles'; -import { toEnvironmentCommands, toEnvironmentScript } from './environment-form'; +import { + hasEnvironmentFormChanges, + toEnvironmentCommands, + toEnvironmentScript, +} from './environment-form'; +import { EnvironmentBuildPanel } from './EnvironmentBuildPanel'; import { buildEnvironmentVariableChanges, formatEnvironmentVariables, @@ -45,6 +50,7 @@ type EnvironmentDialogProps = { repositoryLabel?: string; environmentId: string | null; selectedProfileId?: string | null; + canManage: boolean; onSaved: (environmentId: string) => void; onDeleted: () => void; }; @@ -98,6 +104,7 @@ function EnvironmentForm({ repository, repositoryLabel, selectedProfileId, + canManage, existing, onSaved, onDeleted, @@ -113,9 +120,10 @@ function EnvironmentForm({ const name = existing?.name ?? splitRepositoryName(fullName).name.slice(0, ENVIRONMENT_LIMITS.MAX_NAME_LENGTH); - const [sandboxAllocation, setSandboxAllocation] = useState(() => + const [initialSandboxAllocation] = useState(() => toSelectableAllocation(existing?.sandboxAllocation ?? null) ); + const [sandboxAllocation, setSandboxAllocation] = useState(initialSandboxAllocation); const sandboxRepository = existing ? { platform: existing.platform, fullName } : repository @@ -135,9 +143,18 @@ function EnvironmentForm({ plainVars: existing?.plainVars ?? [], secretKeys: existing?.secretKeys ?? [], })); - const [variablesText, setVariablesText] = useState(() => + const [initialVariablesText] = useState(() => formatEnvironmentVariables(savedVariables.plainVars, savedVariables.secretKeys) ); + const [variablesText, setVariablesText] = useState(initialVariablesText); + const hasUnsavedChanges = hasEnvironmentFormChanges( + { + script: initialScript, + variablesText: initialVariablesText, + sandboxAllocation: initialSandboxAllocation, + }, + { script, variablesText, sandboxAllocation } + ); const [error, setError] = useState(null); let variables: ReturnType | undefined; let variablesError: string | null = null; @@ -279,9 +296,6 @@ function EnvironmentForm({ onClick={() => changePage('variables')} /> -

- Applies automatically to new sessions in this repository. -

)} {page === 'scripts' && } @@ -294,6 +308,17 @@ function EnvironmentForm({ /> )} + {page === 'overview' && + (existing ? ( + + ) : ( +

Save to enable snapshot builds.

+ ))} {profileQuery.isError ? (

Could not check profile settings.{' '} diff --git a/apps/web/src/components/cloud-agent-next/EnvironmentSnapshotNotice.tsx b/apps/web/src/components/cloud-agent-next/EnvironmentSnapshotNotice.tsx new file mode 100644 index 00000000000..db0fb7cd7ce --- /dev/null +++ b/apps/web/src/components/cloud-agent-next/EnvironmentSnapshotNotice.tsx @@ -0,0 +1,16 @@ +import { Info } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { SNAPSHOT_REUSE_NOTICE } from './environment-build-display'; + +/** + * The non-blocking notice shown wherever environment or profile setup inputs + * are edited or combined. It never blocks a save or a launch and names no value. + */ +export function EnvironmentSnapshotNotice({ className }: { className?: string }) { + return ( +

+ + {SNAPSHOT_REUSE_NOTICE} +

+ ); +} diff --git a/apps/web/src/components/cloud-agent-next/NewSessionPanel.attachment-handoff.test.ts b/apps/web/src/components/cloud-agent-next/NewSessionPanel.attachment-handoff.test.ts index d140e200b29..4eb678445ab 100644 --- a/apps/web/src/components/cloud-agent-next/NewSessionPanel.attachment-handoff.test.ts +++ b/apps/web/src/components/cloud-agent-next/NewSessionPanel.attachment-handoff.test.ts @@ -143,6 +143,7 @@ jest.mock('@/hooks/useCloudAgentProfiles', () => ({ useProfiles: () => ({ data: [] }), useCombinedProfiles: () => ({ data: undefined }), useProfile: () => ({ data: undefined }), + useRepoBindings: () => ({ data: [] }), })); jest.mock('@/hooks/useRefreshRepositories', () => ({ diff --git a/apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx b/apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx index fcf5caa0eae..3b9c390f646 100644 --- a/apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx +++ b/apps/web/src/components/cloud-agent-next/NewSessionPanel.tsx @@ -49,8 +49,9 @@ import { VariantCombobox } from '@/components/shared/VariantCombobox'; import { thinkingEffortLabel } from '@/lib/code-reviews/core/model-variants'; import { InsufficientBalanceBanner } from '@/components/shared/InsufficientBalanceBanner'; import { ProfilePickerPopover } from '@/components/cloud-agent/ProfilePickerPopover'; -import { RepositoryEnvironmentChip } from '@/components/cloud-agent-next/RepositoryEnvironmentChip'; import { EnvironmentProfileSetupNotice } from '@/components/cloud-agent-next/EnvironmentProfileSetupNotice'; +import { EnvironmentSnapshotNotice } from '@/components/cloud-agent-next/EnvironmentSnapshotNotice'; +import { RepositoryEnvironmentChip } from '@/components/cloud-agent-next/RepositoryEnvironmentChip'; import { useRepositoryEnvironment, type EnvironmentRepository, @@ -1011,6 +1012,7 @@ export function NewSessionPanel({ organizationId }: NewSessionPanelProps) { organizationId, repository: environmentRepository, }); + const showSnapshotNotice = !!repositoryEnvironment; const creationInput = useMemo(() => { const trimmed = prompt.trim(); @@ -1807,6 +1809,7 @@ export function NewSessionPanel({ organizationId }: NewSessionPanelProps) { ))} )} + {showSnapshotNotice && } {githubIdentityHint && ( diff --git a/apps/web/src/components/cloud-agent-next/RepositoryEnvironmentChip.tsx b/apps/web/src/components/cloud-agent-next/RepositoryEnvironmentChip.tsx index dd0149f4f87..06644a7a735 100644 --- a/apps/web/src/components/cloud-agent-next/RepositoryEnvironmentChip.tsx +++ b/apps/web/src/components/cloud-agent-next/RepositoryEnvironmentChip.tsx @@ -85,6 +85,7 @@ export function RepositoryEnvironmentChip({ repositoryLabel={repositoryLabel} environmentId={environment?.id ?? null} selectedProfileId={selectedProfileId} + canManage={canManage} onSaved={() => setDialogOpen(false)} onDeleted={() => setDialogOpen(false)} /> diff --git a/apps/web/src/components/cloud-agent-next/environment-build-display.test.ts b/apps/web/src/components/cloud-agent-next/environment-build-display.test.ts new file mode 100644 index 00000000000..468dc0c01cc --- /dev/null +++ b/apps/web/src/components/cloud-agent-next/environment-build-display.test.ts @@ -0,0 +1,146 @@ +import { + backoffText, + buildAction, + buildStageLabel, + failureText, + formatAge, + setupBudgetWarning, + snapshotBadge, + stalenessText, + type EnvironmentBuildStatus, + type EnvironmentBuildSummary, +} from './environment-build-display'; + +const NOW = 1_800_000_000_000; + +function summary(overrides: Partial = {}): EnvironmentBuildSummary { + return { + buildId: '11111111-1111-4111-8111-111111111111', + revision: 2, + trigger: 'manual', + state: 'preparing', + outcome: null, + step: null, + failureReason: null, + failureDetail: null, + sourceCommit: null, + sourceBranch: null, + setupDurationMs: null, + createdAt: NOW, + finishedAt: null, + cleanup: 'not_started', + logTruncated: false, + ...overrides, + }; +} + +function status(overrides: Partial = {}): EnvironmentBuildStatus { + return { + environmentId: '22222222-2222-4222-8222-222222222222', + supported: true, + unsupportedReason: null, + configurationRevision: 2, + activeBuild: null, + latestBuild: null, + published: null, + automaticBackoff: null, + sessionPreparationBudgetMs: 12 * 60_000, + ...overrides, + }; +} + +const PUBLISHED = { + buildId: '33333333-3333-4333-8333-333333333333', + revision: 2, + sourceCommit: 'a'.repeat(40), + sourceBranch: 'main', + capturedAt: NOW - 3 * 60 * 60_000, + size: 100, + setupDurationMs: 60_000, + stale: null, + expiresAt: NOW, +}; + +describe('environment build display', () => { + it('names the active stage from the coordinator state and builder step', () => { + expect(buildStageLabel(summary({ state: 'queued' }))).toBe('Queued'); + expect(buildStageLabel(summary({ step: 'setup' }))).toBe('Running setup commands'); + expect(buildStageLabel(summary({ state: 'capturing' }))).toBe('Capturing the snapshot'); + expect(buildStageLabel(summary({ state: 'stopping', outcome: 'succeeded' }))).toBe( + 'Published; stopping the builder' + ); + expect( + buildStageLabel(summary({ state: 'stopping', outcome: 'failed', cleanup: 'failing' })) + ).toBe('Stopping the builder (retrying)'); + expect(buildStageLabel(summary({ state: 'obsolete' }))).toBe('Superseded'); + }); + + it('offers Build, Refresh or Cancel and requires saved configuration', () => { + const options = { canManage: true, hasUnsavedChanges: false }; + expect(buildAction(status(), options)).toEqual({ kind: 'build', disabledReason: null }); + expect(buildAction(status({ published: PUBLISHED }), options)).toEqual({ + kind: 'refresh', + disabledReason: null, + }); + expect(buildAction(status(), { canManage: true, hasUnsavedChanges: true })).toEqual({ + kind: 'build', + disabledReason: 'Save your changes before building.', + }); + expect(buildAction(status({ activeBuild: summary() }), options)).toEqual({ + kind: 'cancel', + buildId: summary().buildId, + }); + expect( + buildAction(status({ activeBuild: summary({ state: 'stopping' }) }), options) + ).toMatchObject({ + kind: 'none', + }); + expect( + buildAction(status({ supported: false, unsupportedReason: 'Not enabled' }), options) + ).toEqual({ kind: 'none', reason: 'Not enabled' }); + expect(buildAction(status(), { canManage: false, hasUnsavedChanges: false }).kind).toBe('none'); + }); + + it('explains when the snapshot does not include the latest settings', () => { + expect( + stalenessText(status({ published: { ...PUBLISHED, revision: 1, stale: 'revision' } })) + ).toBe('Environment settings have changed. Refresh the snapshot to include them.'); + expect(stalenessText(status({ published: PUBLISHED }))).toBeNull(); + expect(stalenessText(status({ published: { ...PUBLISHED, stale: 'image' } }))).toBe( + 'The snapshot was built for an earlier sandbox image. Sessions start without it until a refresh completes.' + ); + }); + + it('warns when fresh setup exceeds the session preparation budget', () => { + expect(setupBudgetWarning(status({ published: PUBLISHED }))).toBeNull(); + expect( + setupBudgetWarning(status({ published: { ...PUBLISHED, setupDurationMs: 15 * 60_000 } })) + ).toBe( + 'Fresh setup took 15m, longer than the 12m a session start allows. Sessions without the snapshot would time out.' + ); + }); + + it('names the automatic backoff without blocking a manual build', () => { + const text = backoffText({ revision: 5, failures: 1, nextAttemptAt: NOW }); + expect(text).toMatch( + /^Automatic builds paused after 1 failed attempt\. Next attempt: .+\. You can still build manually\.$/ + ); + expect(backoffText({ revision: 5, failures: 3, nextAttemptAt: NOW })).toContain( + 'after 3 failed attempts.' + ); + }); + + it('summarizes the row badge and failures', () => { + expect(snapshotBadge(status({ published: PUBLISHED }), NOW)).toBe('Snapshot 3 h ago'); + expect(snapshotBadge(status({ activeBuild: summary() }), NOW)).toBe('Building snapshot'); + expect(snapshotBadge(status({ latestBuild: summary({ state: 'failed' }) }), NOW)).toBe( + 'Snapshot build failed' + ); + expect(snapshotBadge(status({ supported: false }), NOW)).toBeNull(); + expect(failureText('setup_failed')).toBe('A setup command failed. See the build log.'); + expect(failureText('runtime_base_incompatible')).toBe( + 'The selected runtime release could not be installed.' + ); + expect(formatAge(NOW - 30_000, NOW)).toBe('just now'); + }); +}); diff --git a/apps/web/src/components/cloud-agent-next/environment-build-display.ts b/apps/web/src/components/cloud-agent-next/environment-build-display.ts new file mode 100644 index 00000000000..e840ab53a31 --- /dev/null +++ b/apps/web/src/components/cloud-agent-next/environment-build-display.ts @@ -0,0 +1,190 @@ +import type { + EnvironmentBuildRequestResult, + EnvironmentBuildStatus, + EnvironmentBuildSummary, +} from '../../../../../services/cloud-agent-next/src/shared/environment-build-status'; + +/** + * Presentation rules for environment snapshot builds, kept free of React so + * they are unit-tested. Copy follows the plan's environment settings notices. + */ + +export type { EnvironmentBuildRequestResult, EnvironmentBuildStatus, EnvironmentBuildSummary }; + +export const SNAPSHOT_REUSE_NOTICE = + 'Cached files may retain old values, including secrets. Refresh the snapshot to replace them.'; + +const ACTIVE_STATES: ReadonlySet = new Set([ + 'queued', + 'preparing', + 'capturing', + 'publishing', + 'stopping', +]); + +export function isBuildActive(summary: EnvironmentBuildSummary | null | undefined): boolean { + return summary !== null && summary !== undefined && ACTIVE_STATES.has(summary.state); +} + +/** The one line that names what a build is doing now, or how it ended. */ +export function buildStageLabel(summary: EnvironmentBuildSummary): string { + switch (summary.state) { + case 'queued': + return 'Queued'; + case 'preparing': + if (summary.step === 'clone') return 'Cloning the default branch'; + if (summary.step === 'setup') return 'Running setup commands'; + if (summary.step === 'cleanup') return 'Removing credentials'; + return 'Starting the builder'; + case 'capturing': + return 'Capturing the snapshot'; + case 'publishing': + return 'Publishing the snapshot'; + case 'stopping': + switch (summary.outcome) { + case 'succeeded': + return 'Published; stopping the builder'; + case 'canceled': + return 'Canceling'; + default: + return summary.cleanup === 'failing' + ? 'Stopping the builder (retrying)' + : 'Stopping the builder'; + } + case 'succeeded': + return 'Succeeded'; + case 'failed': + return 'Failed'; + case 'canceled': + return 'Canceled'; + case 'obsolete': + return 'Superseded'; + } +} + +const FAILURE_TEXT: Record = { + clone_failed: 'The repository could not be cloned.', + setup_failed: 'A setup command failed. See the build log.', + setup_timeout: 'A setup command timed out. See the build log.', + cleanup_failed: 'Credentials could not be removed, so no snapshot was taken.', + capture_failed: 'The snapshot could not be captured.', + capture_uncertain: 'The snapshot result was lost; the build was not published.', + deadline: 'The build did not finish within 30 minutes.', + builder_lost: 'The builder stopped reporting.', + builder_start_failed: 'The builder could not start.', + input_unavailable: 'The builder could not read its inputs.', + repository_unauthorized: 'Repository access is no longer authorized.', + actor_unauthorized: 'The person who started the build no longer has access.', + billing_blocked: 'Builds need available credits.', + billing_unavailable: 'Billing could not admit the build. Try again.', + container_limit_reached: 'The concurrent container limit is reached.', + configuration_changed: 'The configuration changed during the build.', + environment_deleted: 'The environment was deleted.', + canceled: 'The build was canceled.', + not_found: 'The environment was not found.', + unsupported_provider: 'Snapshot builds are unavailable for this account.', +}; + +export function failureText(reason: string | null): string | null { + if (reason === null) return null; + if (reason.startsWith('runtime_')) return 'The selected runtime release could not be installed.'; + return FAILURE_TEXT[reason] ?? 'The build failed.'; +} + +export function formatDuration(ms: number): string { + const seconds = Math.round(ms / 1000); + if (seconds < 60) return `${seconds}s`; + const minutes = Math.floor(seconds / 60); + const rest = seconds % 60; + return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`; +} + +export function formatAge(timestamp: number, now: number): string { + const minutes = Math.max(0, Math.floor((now - timestamp) / 60_000)); + if (minutes < 1) return 'just now'; + if (minutes < 60) return `${minutes} min ago`; + const hours = Math.floor(minutes / 60); + if (hours < 48) return `${hours} h ago`; + return `${Math.floor(hours / 24)} days ago`; +} + +/** A fresh session start would time out when setup alone exceeds its budget. */ +export function setupBudgetWarning(status: EnvironmentBuildStatus): string | null { + const duration = status.published?.setupDurationMs ?? status.latestBuild?.setupDurationMs ?? null; + if (duration === null || duration <= status.sessionPreparationBudgetMs) return null; + return `Fresh setup took ${formatDuration(duration)}, longer than the ${formatDuration( + status.sessionPreparationBudgetMs + )} a session start allows. Sessions without the snapshot would time out.`; +} + +export function stalenessText(status: EnvironmentBuildStatus): string | null { + switch (status.published?.stale) { + case 'revision': + return 'Environment settings have changed. Refresh the snapshot to include them.'; + case 'age': + return 'The snapshot is older than four days; the next session starts a refresh.'; + case 'image': + return 'The snapshot was built for an earlier sandbox image. Sessions start without it until a refresh completes.'; + default: + return null; + } +} + +/** Automatic builds paused for a revision; a manual build still runs. */ +export function backoffText( + backoff: NonNullable +): string { + const attempts = + backoff.failures === 1 ? '1 failed attempt' : `${backoff.failures} failed attempts`; + const at = new Date(backoff.nextAttemptAt).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); + return `Automatic builds paused after ${attempts}. Next attempt: ${at}. You can still build manually.`; +} + +export type BuildAction = + | { kind: 'build' | 'refresh'; disabledReason: string | null } + | { kind: 'cancel'; buildId: string } + | { kind: 'none'; reason: string }; + +/** + * The one control the build detail and the environment menu show. A build needs + * saved configuration and a supported provider; a running build can be canceled; + * a build that is cleaning up blocks a new one. + */ +export function buildAction( + status: EnvironmentBuildStatus | undefined, + options: { canManage: boolean; hasUnsavedChanges: boolean } +): BuildAction { + if (!options.canManage) return { kind: 'none', reason: 'Only managers can build snapshots.' }; + if (status === undefined) return { kind: 'none', reason: 'Loading build status.' }; + if (!status.supported) { + return { kind: 'none', reason: status.unsupportedReason ?? 'Snapshot builds are unavailable.' }; + } + const active = status.activeBuild; + if (active !== null) { + if (active.state === 'stopping') { + return { kind: 'none', reason: 'Wait for the builder to stop before building again.' }; + } + return { kind: 'cancel', buildId: active.buildId }; + } + return { + kind: status.published === null ? 'build' : 'refresh', + disabledReason: options.hasUnsavedChanges ? 'Save your changes before building.' : null, + }; +} + +/** Short status for the environment list row. */ +export function snapshotBadge( + status: EnvironmentBuildStatus | undefined, + now: number +): string | null { + if (status === undefined || !status.supported) return null; + if (isBuildActive(status.activeBuild)) return 'Building snapshot'; + if (status.published === null) { + return status.latestBuild?.state === 'failed' ? 'Snapshot build failed' : 'No snapshot'; + } + const age = formatAge(status.published.capturedAt, now); + return status.published.stale === null ? `Snapshot ${age}` : `Snapshot ${age} (stale)`; +} diff --git a/apps/web/src/components/cloud-agent-next/environment-display.test.ts b/apps/web/src/components/cloud-agent-next/environment-display.test.ts index 1587131a81d..bb743893c0c 100644 --- a/apps/web/src/components/cloud-agent-next/environment-display.test.ts +++ b/apps/web/src/components/cloud-agent-next/environment-display.test.ts @@ -17,6 +17,7 @@ function summary(partial: Partial): EnvironmentSummary { secretCount: 0, commandCount: 0, sandboxAllocation: null, + configurationRevision: 1, ...partial, }; } diff --git a/apps/web/src/components/cloud-agent-next/environment-form.test.ts b/apps/web/src/components/cloud-agent-next/environment-form.test.ts index 85aceac47ec..e423bc98734 100644 --- a/apps/web/src/components/cloud-agent-next/environment-form.test.ts +++ b/apps/web/src/components/cloud-agent-next/environment-form.test.ts @@ -1,6 +1,10 @@ import { execFileSync } from 'node:child_process'; import { describe, expect, it } from '@jest/globals'; -import { toEnvironmentCommands, toEnvironmentScript } from './environment-form'; +import { + hasEnvironmentFormChanges, + toEnvironmentCommands, + toEnvironmentScript, +} from './environment-form'; describe('single environment script', () => { it('saves one multiline script and removes an empty script', () => { @@ -33,3 +37,33 @@ describe('single environment script', () => { } }); }); + +describe('hasEnvironmentFormChanges', () => { + const saved = { + script: toEnvironmentScript(['export A=1', 'echo "$A"']), + variablesText: 'TOKEN=\nPLAIN="saved"', + sandboxAllocation: 'cloudflare-single', + }; + + test('allows a build when the legacy script and secret markers stay untouched', () => { + expect(hasEnvironmentFormChanges(saved, { ...saved })).toBe(false); + }); + + test.each([ + { script: 'pnpm install' }, + { variablesText: 'TOKEN="replacement"' }, + { variablesText: 'PLAIN="saved"' }, + { variablesText: 'TOKEN=\nPLAIN="changed"' }, + { variablesText: 'TOKEN="unterminated' }, + { sandboxAllocation: null }, + ])('blocks a build while the editor contains unsaved changes: %j', change => { + expect(hasEnvironmentFormChanges(saved, { ...saved, ...change })).toBe(true); + }); + + test('allows a build after reverting the edits', () => { + const draft = { ...saved, script: 'changed' }; + expect(hasEnvironmentFormChanges(saved, draft)).toBe(true); + draft.script = saved.script; + expect(hasEnvironmentFormChanges(saved, draft)).toBe(false); + }); +}); diff --git a/apps/web/src/components/cloud-agent-next/environment-form.ts b/apps/web/src/components/cloud-agent-next/environment-form.ts index 0b3a25e43ed..f6c96795c9b 100644 --- a/apps/web/src/components/cloud-agent-next/environment-form.ts +++ b/apps/web/src/components/cloud-agent-next/environment-form.ts @@ -10,3 +10,15 @@ export function toEnvironmentCommands(script: string): string[] { const trimmed = script.trim(); return trimmed ? [trimmed] : []; } + +/** Builds use saved configuration. Keep the editor baseline stable during status polling. */ +export function hasEnvironmentFormChanges( + saved: { script: string; variablesText: string; sandboxAllocation: string | null }, + draft: { script: string; variablesText: string; sandboxAllocation: string | null } +): boolean { + return ( + saved.script !== draft.script || + saved.variablesText !== draft.variablesText || + saved.sandboxAllocation !== draft.sandboxAllocation + ); +} diff --git a/apps/web/src/components/cloud-agent-next/settings/EnvironmentsSection.tsx b/apps/web/src/components/cloud-agent-next/settings/EnvironmentsSection.tsx index c2024f72bf1..d33ae08558c 100644 --- a/apps/web/src/components/cloud-agent-next/settings/EnvironmentsSection.tsx +++ b/apps/web/src/components/cloud-agent-next/settings/EnvironmentsSection.tsx @@ -2,11 +2,29 @@ import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { Boxes, GitBranch, Loader2, Pencil, Plus, Trash2 } from 'lucide-react'; +import { + Boxes, + GitBranch, + Hammer, + Loader2, + MoreHorizontal, + Pencil, + Plus, + RefreshCw, + Square, + Trash2, +} from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { AlertDialog, AlertDialogAction, @@ -29,11 +47,14 @@ import { useTRPC } from '@/lib/trpc/utils'; import { useCloudAgentEnvironmentMutations, useCloudAgentEnvironmentsForOwner, + useEnvironmentBuildStatus, type EnvironmentRepository, type EnvironmentSummary, } from '@/hooks/useCloudAgentEnvironments'; import { findRepositoryEnvironment, formatEnvironmentCounts } from '../environment-display'; import { EnvironmentDialog } from '../EnvironmentDialog'; +import { useEnvironmentBuildControl } from '../EnvironmentBuildPanel'; +import { buildAction, snapshotBadge } from '../environment-build-display'; type EnvironmentsSectionProps = { organizationId?: string; @@ -140,7 +161,8 @@ export function EnvironmentsSection({ organizationId, canManage }: EnvironmentsS

Environments

- Saved repository setup, applied to new sessions automatically. + Each repository can have one environment: setup commands, variables, secrets, and a + sandbox size. New sessions in the repository use it automatically.

{canManage && ( @@ -229,6 +251,7 @@ export function EnvironmentsSection({ organizationId, canManage }: EnvironmentsS openEdit(environment.id)} onDelete={() => setPendingDelete(environment)} @@ -244,6 +267,7 @@ export function EnvironmentsSection({ organizationId, canManage }: EnvironmentsS repository={dialogRepository} repositoryLabel={dialogRepository ? repositoryLabel(dialogRepository) : undefined} environmentId={dialogEnvironmentId} + canManage={canManage} onSaved={() => setDialogOpen(false)} onDeleted={() => setDialogOpen(false)} /> @@ -279,27 +303,36 @@ export function EnvironmentsSection({ organizationId, canManage }: EnvironmentsS function EnvironmentRow({ environment, + organizationId, canManage, onEdit, onDelete, }: { environment: EnvironmentSummary; + organizationId?: string; canManage: boolean; onEdit: () => void; onDelete: () => void; }) { const meta = formatEnvironmentCounts(environment); + const { data: buildStatus } = useEnvironmentBuildStatus(environment.id, { organizationId }); + const badge = snapshotBadge(buildStatus, Date.now()); return (
-
+
{environment.name} {platformLabel(environment.platform)} + {badge && ( + + {badge} + + )}

{environment.repoFullName} @@ -307,31 +340,84 @@ function EnvironmentRow({ {meta &&

{meta}

}
{canManage && ( -
- - -
+ )}
); } +function EnvironmentRowMenu({ + environment, + organizationId, + buildStatus, + onEdit, + onDelete, +}: { + environment: EnvironmentSummary; + organizationId?: string; + buildStatus: ReturnType['data']; + onEdit: () => void; + onDelete: () => void; +}) { + const control = useEnvironmentBuildControl({ + organizationId, + environmentId: environment.id, + status: buildStatus, + }); + const action = buildAction(buildStatus, { canManage: true, hasUnsavedChanges: false }); + return ( + + + + + + + + Edit + + {(action.kind === 'build' || action.kind === 'refresh') && ( + void control.start()}> + {action.kind === 'refresh' ? ( + + ) : ( + + )} + {action.kind === 'refresh' ? 'Refresh snapshot' : 'Build snapshot'} + + )} + {action.kind === 'cancel' && ( + control.cancel(action.buildId)} + > + + Cancel build + + )} + + + + Delete + + + + ); +} + function repositoryLabel(repository: EnvironmentRepository): string { return repository.platform === 'gitlab' ? repository.projectPath : repository.fullName; } diff --git a/apps/web/src/components/cloud-agent/ProfilePickerPopover.tsx b/apps/web/src/components/cloud-agent/ProfilePickerPopover.tsx index 99294090d53..38034b7e571 100644 --- a/apps/web/src/components/cloud-agent/ProfilePickerPopover.tsx +++ b/apps/web/src/components/cloud-agent/ProfilePickerPopover.tsx @@ -16,13 +16,8 @@ import { import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; -import { - useProfiles, - useCombinedProfiles, - useRepoBindings, - type ProfileSummaryWithOwner, -} from '@/hooks/useCloudAgentProfiles'; -import { resolveProfileLayers } from '@kilocode/cloud-agent-profile'; +import { type ProfileSummaryWithOwner } from '@/hooks/useCloudAgentProfiles'; +import { useSessionProfileLayers } from '@/hooks/useSessionProfileLayers'; import { ProfilesListDialog } from './ProfilesListDialog'; type ProfilePickerPopoverProps = { @@ -59,76 +54,20 @@ export function ProfilePickerPopover({ }; const { - data: combinedData, - isLoading: combinedProfilesLoading, - error: combinedProfilesError, - refetch: refetchCombinedProfiles, - } = useCombinedProfiles({ - organizationId: organizationId ?? '', - enabled: !!organizationId, - }); - const { - data: personalProfilesData, - isLoading: personalProfilesLoading, - error: personalProfilesError, - refetch: refetchPersonalProfiles, - } = useProfiles({ - organizationId: undefined, - enabled: !organizationId, + allProfiles, + profilesData, + profilesLoading, + profilesError, + refetchProfiles, + layers, + baseProfile, + topProfile, + } = useSessionProfileLayers({ + organizationId, + selectedOverrideProfileId, + repoFullName, + platform, }); - const profilesLoading = organizationId ? combinedProfilesLoading : personalProfilesLoading; - const profilesError = organizationId ? combinedProfilesError : personalProfilesError; - // React Query keeps `error` set while retaining `data` when a background - // refetch fails (a window-focus refetch, or the invalidation refetch a profile - // mutation triggers). Only surface the retry state when there is nothing - // cached to show, so a transient failure never blanks a working picker. - const profilesData = organizationId ? combinedData : personalProfilesData; - const refetchProfiles = organizationId ? refetchCombinedProfiles : refetchPersonalProfiles; - - const allProfiles: ProfileSummaryWithOwner[] = useMemo( - () => - organizationId - ? (combinedData?.allProfiles ?? []) - : (personalProfilesData ?? []).map(p => ({ ...p, ownerType: 'user' as const })), - [organizationId, combinedData, personalProfilesData] - ); - - const effectiveDefaultProfileId = organizationId - ? (combinedData?.effectiveDefaultId ?? null) - : (personalProfilesData?.find(p => p.isDefault)?.id ?? null); - - const { data: repoBindings } = useRepoBindings({ organizationId }); - - const repoBindingProfileId = useMemo(() => { - if (!repoFullName || !repoBindings) return null; - const binding = repoBindings.find( - b => - b.repoFullName.toLowerCase() === repoFullName.toLowerCase() && - (!platform || b.platform === platform) - ); - return binding?.profileId ?? null; - }, [repoFullName, repoBindings, platform]); - - // Shared resolution logic — same rules as the server-side merge. - const layers = useMemo( - () => - resolveProfileLayers({ - repoBindingProfileId, - effectiveDefaultProfileId, - explicitOverrideProfileId: selectedOverrideProfileId, - }), - [repoBindingProfileId, effectiveDefaultProfileId, selectedOverrideProfileId] - ); - - const baseProfile = useMemo( - () => (layers.base ? (allProfiles.find(p => p.id === layers.base?.profileId) ?? null) : null), - [allProfiles, layers.base] - ); - - const topProfile = useMemo( - () => (layers.top ? (allProfiles.find(p => p.id === layers.top?.profileId) ?? null) : null), - [allProfiles, layers.top] - ); const topSource = layers.top?.source ?? null; // Profiles offered as override candidates. The base profile is omitted diff --git a/apps/web/src/components/cloud-agent/profile-editor/CommandsTab.tsx b/apps/web/src/components/cloud-agent/profile-editor/CommandsTab.tsx index 9010513f5de..f8f5a3d84ef 100644 --- a/apps/web/src/components/cloud-agent/profile-editor/CommandsTab.tsx +++ b/apps/web/src/components/cloud-agent/profile-editor/CommandsTab.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { toast } from 'sonner'; import { Plus, Loader2, Trash2, Terminal } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { EnvironmentSnapshotNotice } from '@/components/cloud-agent-next/EnvironmentSnapshotNotice'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import type { @@ -74,6 +75,7 @@ export function CommandsTab({ Setup Commands
+ {commands.map((cmd, i) => (
Environment Variables
+ {vars.map(varItem => (
(poll && query.state.data?.activeBuild ? BUILD_POLL_MS : false), + } + ) + ); +} + +/** The latest retained build output, refreshed while the build is running. */ +export function useEnvironmentBuildLogs( + environmentId: string | null, + buildId: string | null, + options: UseEnvironmentOptions & { live?: boolean } = {} +) { + const { organizationId, enabled = true, live = false } = options; + const trpc = useTRPC(); + return useQuery( + trpc.cloudEnvironments.buildLogs.queryOptions( + { organizationId, environmentId: environmentId ?? '', buildId: buildId ?? '' }, + { + enabled: enabled && !!environmentId && !!buildId, + refetchInterval: live ? BUILD_POLL_MS : false, + } + ) + ); +} + +/** + * Build/Refresh and Cancel. A Build request keeps its request id until the + * server answers, so a retry after a lost response cannot start a second build. + */ +export function useEnvironmentBuildMutations() { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + // Survives re-renders: a retried Build after a lost response reuses its id. + const pendingRequestIds = useRef(new Map()).current; + + const invalidate = async () => { + await queryClient.invalidateQueries({ + queryKey: trpc.cloudEnvironments.buildStatus.queryKey(), + }); + await queryClient.invalidateQueries({ queryKey: trpc.cloudEnvironments.buildLogs.queryKey() }); + }; + + const buildMutation = useMutation( + trpc.cloudEnvironments.build.mutationOptions({ onSettled: invalidate }) + ); + const cancelBuild = useMutation( + trpc.cloudEnvironments.cancelBuild.mutationOptions({ onSettled: invalidate }) + ); + + const requestBuild = async (input: { + organizationId?: string; + environmentId: string; + expectedRevision: number; + }) => { + const key = `${input.environmentId}:${input.expectedRevision}`; + const requestId = pendingRequestIds.get(key) ?? crypto.randomUUID(); + pendingRequestIds.set(key, requestId); + const result = await buildMutation.mutateAsync({ ...input, requestId }); + pendingRequestIds.delete(key); + return result; + }; + + return { requestBuild, isRequesting: buildMutation.isPending, cancelBuild }; +} diff --git a/apps/web/src/hooks/useSessionProfileLayers.ts b/apps/web/src/hooks/useSessionProfileLayers.ts new file mode 100644 index 00000000000..363785d4f1a --- /dev/null +++ b/apps/web/src/hooks/useSessionProfileLayers.ts @@ -0,0 +1,108 @@ +/** Resolves the profile layers a new session applies, with the same rules as the server merge. */ +'use client'; + +import { useMemo } from 'react'; +import { resolveProfileLayers } from '@kilocode/cloud-agent-profile'; +import { + useCombinedProfiles, + useProfiles, + useRepoBindings, + type ProfileSummaryWithOwner, +} from '@/hooks/useCloudAgentProfiles'; + +type SessionProfileLayersInput = { + organizationId?: string; + selectedOverrideProfileId: string | null; + repoFullName?: string; + platform?: 'github' | 'gitlab'; +}; + +export function useSessionProfileLayers({ + organizationId, + selectedOverrideProfileId, + repoFullName, + platform, +}: SessionProfileLayersInput) { + const { + data: combinedData, + isLoading: combinedProfilesLoading, + error: combinedProfilesError, + refetch: refetchCombinedProfiles, + } = useCombinedProfiles({ + organizationId: organizationId ?? '', + enabled: !!organizationId, + }); + const { + data: personalProfilesData, + isLoading: personalProfilesLoading, + error: personalProfilesError, + refetch: refetchPersonalProfiles, + } = useProfiles({ + organizationId: undefined, + enabled: !organizationId, + }); + const profilesLoading = organizationId ? combinedProfilesLoading : personalProfilesLoading; + const profilesError = organizationId ? combinedProfilesError : personalProfilesError; + // React Query keeps `error` set while retaining `data` when a background + // refetch fails (a window-focus refetch, or the invalidation refetch a profile + // mutation triggers). Only surface the retry state when there is nothing + // cached to show, so a transient failure never blanks a working picker. + const profilesData = organizationId ? combinedData : personalProfilesData; + const refetchProfiles = organizationId ? refetchCombinedProfiles : refetchPersonalProfiles; + + const allProfiles: ProfileSummaryWithOwner[] = useMemo( + () => + organizationId + ? (combinedData?.allProfiles ?? []) + : (personalProfilesData ?? []).map(p => ({ ...p, ownerType: 'user' as const })), + [organizationId, combinedData, personalProfilesData] + ); + + const effectiveDefaultProfileId = organizationId + ? (combinedData?.effectiveDefaultId ?? null) + : (personalProfilesData?.find(p => p.isDefault)?.id ?? null); + + const { data: repoBindings } = useRepoBindings({ organizationId }); + + const repoBindingProfileId = useMemo(() => { + if (!repoFullName || !repoBindings) return null; + const binding = repoBindings.find( + b => + b.repoFullName.toLowerCase() === repoFullName.toLowerCase() && + (!platform || b.platform === platform) + ); + return binding?.profileId ?? null; + }, [repoFullName, repoBindings, platform]); + + // Shared resolution logic — same rules as the server-side merge. + const layers = useMemo( + () => + resolveProfileLayers({ + repoBindingProfileId, + effectiveDefaultProfileId, + explicitOverrideProfileId: selectedOverrideProfileId, + }), + [repoBindingProfileId, effectiveDefaultProfileId, selectedOverrideProfileId] + ); + + const baseProfile = useMemo( + () => (layers.base ? (allProfiles.find(p => p.id === layers.base?.profileId) ?? null) : null), + [allProfiles, layers.base] + ); + + const topProfile = useMemo( + () => (layers.top ? (allProfiles.find(p => p.id === layers.top?.profileId) ?? null) : null), + [allProfiles, layers.top] + ); + + return { + allProfiles, + profilesData, + profilesLoading, + profilesError, + refetchProfiles, + layers, + baseProfile, + topProfile, + }; +} diff --git a/apps/web/src/lib/cloud-agent-next/environment-build-client.ts b/apps/web/src/lib/cloud-agent-next/environment-build-client.ts new file mode 100644 index 00000000000..78ac9ca4fff --- /dev/null +++ b/apps/web/src/lib/cloud-agent-next/environment-build-client.ts @@ -0,0 +1,87 @@ +import 'server-only'; +import { + environmentBuildCancelResultSchema, + environmentBuildLogsSchema, + environmentBuildRequestResultSchema, + environmentBuildStatusSchema, + type EnvironmentBuildCancelResult, + type EnvironmentBuildLogs, + type EnvironmentBuildRequestResult, + type EnvironmentBuildStatus, +} from '../../../../../services/cloud-agent-next/src/shared/environment-build-status'; +import { createCloudAgentNextClient, rethrowCloudAgentError } from './cloud-agent-client'; + +/** + * Environment build calls to the cloud-agent-next Worker. The Worker + * authorizes each one again (owner, organization role, repository); the web + * router's role check only shapes the UI's errors. + */ + +type Scope = { environmentId: string; kilocodeOrganizationId?: string }; + +type EnvironmentBuildProcedures = { + buildEnvironment: { + mutate(input: Scope & { expectedRevision: number; requestId: string }): Promise; + }; + getEnvironmentBuildStatus: { query(input: Scope): Promise }; + getEnvironmentBuildLogs: { + query(input: Scope & { buildId: string; cursor?: number; limit: number }): Promise; + }; + cancelEnvironmentBuild: { mutate(input: Scope & { buildId: string }): Promise }; + notifyEnvironmentDeleted: { mutate(input: { environmentId: string }): Promise }; +}; + +export type EnvironmentBuildClient = { + build( + input: Scope & { expectedRevision: number; requestId: string } + ): Promise; + status(input: Scope): Promise; + logs( + input: Scope & { buildId: string; cursor?: number; limit: number } + ): Promise; + cancel(input: Scope & { buildId: string }): Promise; + environmentDeleted(environmentId: string): Promise; +}; + +async function call( + operation: () => Promise, + parse: (value: unknown) => T +): Promise { + try { + return parse(await operation()); + } catch (error) { + rethrowCloudAgentError(error); + } +} + +export function createEnvironmentBuildClient(authToken: string): EnvironmentBuildClient { + const client = createCloudAgentNextClient(authToken).getClient() as EnvironmentBuildProcedures; + return { + build: input => + call( + () => client.buildEnvironment.mutate(input), + value => environmentBuildRequestResultSchema.parse(value) + ), + status: input => + call( + () => client.getEnvironmentBuildStatus.query(input), + value => environmentBuildStatusSchema.parse(value) + ), + logs: input => + call( + () => client.getEnvironmentBuildLogs.query(input), + value => environmentBuildLogsSchema.parse(value) + ), + cancel: input => + call( + () => client.cancelEnvironmentBuild.mutate(input), + value => environmentBuildCancelResultSchema.parse(value) + ), + environmentDeleted: async environmentId => { + await call( + () => client.notifyEnvironmentDeleted.mutate({ environmentId }), + () => undefined + ); + }, + }; +} diff --git a/apps/web/src/routers/cloud-environments-router.test.ts b/apps/web/src/routers/cloud-environments-router.test.ts index 800b0f194f5..881f7c73b1a 100644 --- a/apps/web/src/routers/cloud-environments-router.test.ts +++ b/apps/web/src/routers/cloud-environments-router.test.ts @@ -82,6 +82,16 @@ describe('cloudEnvironmentsRouter', () => { await cleanupDbForTest(); }); + // The router graph loads after the mocks above, so it uses them. + beforeAll(async () => { + ({ createCallerForUser } = await import('@/routers/test-utils')); + }); + + afterEach(async () => { + mockGetSandboxSelectionOptions.mockImplementation(async input => offerAll(input)); + await cleanupDbForTest(); + }); + test('a member can list but cannot create, while owner and admin can create, update, and delete', async () => { const owner = await insertTestUser(); const member = await insertTestUser(); @@ -346,7 +356,11 @@ describe('cloudEnvironmentsRouter', () => { const [summary] = await caller.cloudEnvironments.list({ repository }); expect(summary).toMatchObject({ sandboxAllocation: 'cloudflare-single' }); - await caller.cloudEnvironments.update({ environmentId: id, sandboxAllocation: null }); + const { configurationRevision } = await caller.cloudEnvironments.update({ + environmentId: id, + sandboxAllocation: null, + }); + expect(configurationRevision).toBe(1); expect( (await caller.cloudEnvironments.get({ environmentId: id })).sandboxAllocation ).toBeNull(); @@ -512,7 +526,7 @@ describe('cloudEnvironmentsRouter', () => { owner: { type: 'user', id: selfHosted.id }, repository: { platform: 'gitlab', cloneUrl: sessionCloneUrl ?? '' }, }); - expect(resolved).not.toBeNull(); + expect(resolved?.reference.environmentId).toBe(id); const comList = await gitlabComCaller.cloudEnvironments.list({ repository: { platform: 'gitlab', projectPath: 'group/repo' }, @@ -581,6 +595,69 @@ describe('cloudEnvironmentsRouter', () => { expect(await storedValue(id, 'PLACEHOLDER')).toBeUndefined(); }); + test('update returns the configuration revision, which only setup changes advance', async () => { + const owner = await insertTestUser(); + const caller = await createCallerForUser(owner.id); + const { id } = await caller.cloudEnvironments.create({ + name: 'revisions', + repository: { platform: 'github', fullName: GITHUB_REPO }, + commands: ['pnpm install'], + }); + expect((await caller.cloudEnvironments.get({ environmentId: id })).configurationRevision).toBe( + 1 + ); + + const renamed = await caller.cloudEnvironments.update({ environmentId: id, name: 'renamed' }); + expect(renamed.configurationRevision).toBe(1); + + const edited = await caller.cloudEnvironments.update({ + environmentId: id, + commands: ['pnpm install --frozen-lockfile'], + }); + expect(edited.configurationRevision).toBe(2); + expect((await caller.cloudEnvironments.get({ environmentId: id })).configurationRevision).toBe( + 2 + ); + }); + + test('a member cannot build, cancel, or read build logs', async () => { + const owner = await insertTestUser(); + const member = await insertTestUser(); + const organization = await createTestOrganization('Environment build org', owner.id, 0); + await addUserToOrganization(organization.id, member.id, 'member'); + const ownerCaller = await createCallerForUser(owner.id); + const { id } = await ownerCaller.cloudEnvironments.create({ + organizationId: organization.id, + name: 'shared', + repository: { platform: 'github', fullName: GITHUB_REPO }, + }); + + const memberCaller = await createCallerForUser(member.id); + const buildId = '11111111-1111-4111-8111-111111111111'; + await expect( + memberCaller.cloudEnvironments.build({ + organizationId: organization.id, + environmentId: id, + expectedRevision: 1, + requestId: '22222222-2222-4222-8222-222222222222', + }) + ).rejects.toMatchObject({ code: 'UNAUTHORIZED' }); + await expect( + memberCaller.cloudEnvironments.cancelBuild({ + organizationId: organization.id, + environmentId: id, + buildId, + }) + ).rejects.toMatchObject({ code: 'UNAUTHORIZED' }); + await expect( + memberCaller.cloudEnvironments.buildLogs({ + organizationId: organization.id, + environmentId: id, + buildId, + }) + ).rejects.toMatchObject({ code: 'UNAUTHORIZED' }); + }); + test('a soft-deleted organization is not found for list, get, and create', async () => { const owner = await insertTestUser(); const organization = await createTestOrganization('Soft-deleted org', owner.id, 0); diff --git a/apps/web/src/routers/cloud-environments-router.ts b/apps/web/src/routers/cloud-environments-router.ts index f66dc6ec381..5f1ed4e9ebe 100644 --- a/apps/web/src/routers/cloud-environments-router.ts +++ b/apps/web/src/routers/cloud-environments-router.ts @@ -27,11 +27,19 @@ import { getGitLabInstanceUrlForOrganization, getGitLabInstanceUrlForUser, } from '@/lib/cloud-agent/gitlab-integration-helpers'; +import { createControlTokenForRequest } from '@/lib/auth/resource-delegation'; +import { createEnvironmentBuildClient } from '@/lib/cloud-agent-next/environment-build-client'; import { assertAllocationOffered, loadSandboxCapabilities, } from '@/lib/cloud-agent-next/sandbox-allocation-offers'; import type { User } from '@kilocode/db/schema'; +import { + environmentBuildCancelResultSchema, + environmentBuildLogsSchema, + environmentBuildRequestResultSchema, + environmentBuildStatusSchema, +} from '../../../../services/cloud-agent-next/src/shared/environment-build-status'; const environmentNameSchema = z.string().trim().min(1).max(ENVIRONMENT_LIMITS.MAX_NAME_LENGTH); const environmentKeySchema = z @@ -97,6 +105,7 @@ const environmentSummarySchema = z.object({ plainVarCount: z.number(), secretCount: z.number(), commandCount: z.number(), + configurationRevision: z.number(), sandboxAllocation: z.string().nullable(), }); @@ -120,6 +129,7 @@ const environmentResponseSchema = z.object({ ), commands: z.array(z.string()), secretKeys: z.array(z.string()), + configurationRevision: z.number(), sandboxAllocation: z.string().nullable(), }); @@ -133,6 +143,20 @@ function getOwner(organizationId: string | undefined, userId: string): Environme : { type: 'user', id: userId }; } +/** A Worker client acting as the caller, scoped to the organization when one is given. */ +async function environmentBuildClientFor( + user: User, + headersList: Headers | undefined, + organizationId: string | undefined +) { + const { token } = await createControlTokenForRequest(user, 'cloud-agent-next', { + headers: headersList, + ...(organizationId ? { organizationId } : {}), + tokenSource: 'cloud-agent', + }); + return createEnvironmentBuildClient(token); +} + /** * Rejects a sandbox the owner is not offered for the repository's platform. * Callers check only a newly chosen key: an unchanged saved key may be stale, @@ -149,6 +173,11 @@ async function assertEnvironmentAllocationOffered( assertAllocationOffered(capabilities, allocation, platform); } +const environmentBuildScopeSchema = z.object({ + organizationId: z.uuid().optional(), + environmentId: z.uuid(), +}); + async function requireActiveOrganization(organizationId: string): Promise { if (!(await getOrganizationById(organizationId))) { throw new TRPCError({ code: 'NOT_FOUND', message: 'Organization not found' }); @@ -343,7 +372,7 @@ export const cloudEnvironmentsRouter = createTRPCRouter({ sandboxAllocation: environmentSandboxAllocationSchema.optional(), }) ) - .output(z.object({ success: z.boolean() })) + .output(z.object({ success: z.boolean(), configurationRevision: z.number() })) .mutation(async ({ ctx, input }) => { if (input.organizationId) { await ensureOrganizationAccess(ctx, input.organizationId, ORGANIZATION_MANAGE_ROLES); @@ -361,7 +390,7 @@ export const cloudEnvironmentsRouter = createTRPCRouter({ ); } } - await db.transaction(tx => + const { configurationRevision } = await db.transaction(tx => updateEnvironment(tx, publicKey, { owner, environmentId: input.environmentId, @@ -372,7 +401,7 @@ export const cloudEnvironmentsRouter = createTRPCRouter({ sandboxAllocation: input.sandboxAllocation, }) ); - return { success: true }; + return { success: true, configurationRevision }; }), delete: baseProcedure @@ -384,6 +413,112 @@ export const cloudEnvironmentsRouter = createTRPCRouter({ } const owner = getOwner(input.organizationId, ctx.user.id); await deleteEnvironment(db, input.environmentId, owner); + // Prompt build cleanup. Best effort: the coordinator also notices the + // deletion on its own, so a failed notice changes nothing. + try { + const builds = await environmentBuildClientFor( + ctx.user, + ctx.headersList, + input.organizationId + ); + await builds.environmentDeleted(input.environmentId); + } catch { + // Ignored; see above. + } return { success: true }; }), + + /** Snapshot build status. Members may read it; it carries no values or handles. */ + buildStatus: baseProcedure + .input(environmentBuildScopeSchema) + .output(environmentBuildStatusSchema) + .query(async ({ ctx, input }) => { + if (input.organizationId) { + await ensureOrganizationAccess(ctx, input.organizationId); + await requireActiveOrganization(input.organizationId); + } + const builds = await environmentBuildClientFor( + ctx.user, + ctx.headersList, + input.organizationId + ); + return builds.status({ + environmentId: input.environmentId, + ...(input.organizationId ? { kilocodeOrganizationId: input.organizationId } : {}), + }); + }), + + /** + * Build or refresh the environment snapshot from the base image. A retried + * request after a lost response reuses its `requestId`. + */ + build: baseProcedure + .input( + environmentBuildScopeSchema.extend({ + expectedRevision: z.number().int().positive(), + requestId: z.uuid(), + }) + ) + .output(environmentBuildRequestResultSchema) + .mutation(async ({ ctx, input }) => { + if (input.organizationId) { + await ensureOrganizationAccess(ctx, input.organizationId, ORGANIZATION_MANAGE_ROLES); + } + const builds = await environmentBuildClientFor( + ctx.user, + ctx.headersList, + input.organizationId + ); + return builds.build({ + environmentId: input.environmentId, + expectedRevision: input.expectedRevision, + requestId: input.requestId, + ...(input.organizationId ? { kilocodeOrganizationId: input.organizationId } : {}), + }); + }), + + buildLogs: baseProcedure + .input( + environmentBuildScopeSchema.extend({ + buildId: z.uuid(), + cursor: z.number().int().nonnegative().optional(), + }) + ) + .output(environmentBuildLogsSchema) + .query(async ({ ctx, input }) => { + if (input.organizationId) { + await ensureOrganizationAccess(ctx, input.organizationId, ORGANIZATION_MANAGE_ROLES); + } + const builds = await environmentBuildClientFor( + ctx.user, + ctx.headersList, + input.organizationId + ); + return builds.logs({ + environmentId: input.environmentId, + buildId: input.buildId, + cursor: input.cursor, + limit: 500, + ...(input.organizationId ? { kilocodeOrganizationId: input.organizationId } : {}), + }); + }), + + cancelBuild: baseProcedure + .input(environmentBuildScopeSchema.extend({ buildId: z.uuid() })) + .output(environmentBuildCancelResultSchema) + .mutation(async ({ ctx, input }) => { + if (input.organizationId) { + await ensureOrganizationAccess(ctx, input.organizationId, ORGANIZATION_MANAGE_ROLES); + } + const builds = await environmentBuildClientFor( + ctx.user, + ctx.headersList, + input.organizationId + ); + return builds.cancel({ + environmentId: input.environmentId, + buildId: input.buildId, + ...(input.organizationId ? { kilocodeOrganizationId: input.organizationId } : {}), + }); + }), }); diff --git a/apps/web/src/tests/cloud-agent-profile/environment-service.test.ts b/apps/web/src/tests/cloud-agent-profile/environment-service.test.ts index 7bf4e3a5810..090fa0664de 100644 --- a/apps/web/src/tests/cloud-agent-profile/environment-service.test.ts +++ b/apps/web/src/tests/cloud-agent-profile/environment-service.test.ts @@ -22,8 +22,10 @@ import { getEnvironment, listEnvironments, listEnvironmentsForOwner, + loadEnvironmentBuildInput, loadEnvironmentForSession, lockActiveOrganization, + readEnvironmentReference, updateEnvironment, ENVIRONMENT_LIMITS, MASKED_SECRET_VALUE, @@ -47,9 +49,10 @@ async function createEnv(input: EnvironmentCreateInput): Promise { }); } -async function updateEnv(input: EnvironmentUpdateInput): Promise { - await db.transaction(async tx => { - await updateEnvironment(tx, publicKeyBase64, input); +async function updateEnv(input: EnvironmentUpdateInput): Promise { + return db.transaction(async tx => { + const { configurationRevision } = await updateEnvironment(tx, publicKeyBase64, input); + return configurationRevision; }); } @@ -690,7 +693,7 @@ describe('repository environment service', () => { }); describe('sandbox allocation', () => { - test('saves, keeps, and clears the size', async () => { + test('saves, keeps, and clears the size without advancing the revision', async () => { const user = await insertTestUser(); const owner = userOwner(user.id); const id = await createEnv({ @@ -701,19 +704,97 @@ describe('repository environment service', () => { repoFullName: 'acme/widgets', sandboxAllocation: 'cloudflare-single', }); - expect((await getEnvironment(db, id, owner)).sandboxAllocation).toBe('cloudflare-single'); + expect(await getEnvironment(db, id, owner)).toMatchObject({ + sandboxAllocation: 'cloudflare-single', + configurationRevision: 1, + }); - await updateEnv({ owner, environmentId: id, sandboxAllocation: 'vercel-large' }); - await updateEnv({ owner, environmentId: id, name: 'Renamed' }); + await expect( + updateEnv({ owner, environmentId: id, sandboxAllocation: 'vercel-large' }) + ).resolves.toBe(1); + await expect(updateEnv({ owner, environmentId: id, name: 'Renamed' })).resolves.toBe(1); expect((await getEnvironment(db, id, owner)).sandboxAllocation).toBe('vercel-large'); - await updateEnv({ owner, environmentId: id, sandboxAllocation: null }); + await expect(updateEnv({ owner, environmentId: id, sandboxAllocation: null })).resolves.toBe( + 1 + ); const [summary] = await listEnvironmentsForOwner(db, owner); - expect(summary?.sandboxAllocation).toBeNull(); + expect(summary).toMatchObject({ sandboxAllocation: null, configurationRevision: 1 }); }); }); describe('loadEnvironmentForSession', () => { + test('does not mix variable values with a concurrent configuration revision', async () => { + const user = await insertTestUser(); + const owner = userOwner(user.id); + const id = await createEnv({ + owner, + createdByUserId: user.id, + name: 'Consistent', + platform: 'github', + repoFullName: 'acme/consistent', + plainVars: { VALUE: 'before' }, + }); + let edited = false; + // Commit an edit after the first real read; keep the SQL and transaction real. + const wrapQuery = (query: object): object => + new Proxy(query, { + get(target, key) { + const value: unknown = Reflect.get(target, key, target); + if (typeof value !== 'function') return value; + if (key === 'then') + return ( + resolve: (value: unknown) => unknown, + reject: (error: unknown) => unknown + ) => { + const read = Reflect.apply(value, target, [ + (rows: unknown) => rows, + ]) as Promise; + return read + .then(async rows => { + if (!edited) { + edited = true; + await updateEnv({ owner, environmentId: id, plainVars: { VALUE: 'after' } }); + } + return rows; + }) + .then(resolve, reject); + }; + return (...args: unknown[]) => { + const next: unknown = Reflect.apply(value, target, args); + return next !== null && typeof next === 'object' ? wrapQuery(next) : next; + }; + }, + }); + const instrument = (database: T): T => + new Proxy(database, { + get(target, key) { + const value: unknown = Reflect.get(target, key, target); + if (typeof value !== 'function') return value; + if (key === 'select') + return (...args: unknown[]) => + wrapQuery(Reflect.apply(value, target, args) as object); + if (key === 'transaction') + return (body: (tx: object) => unknown, ...options: unknown[]) => + Reflect.apply(value, target, [(tx: object) => body(instrument(tx)), ...options]); + return value.bind(target); + }, + }); + const loaded = await loadEnvironmentForSession(instrument(db), { + owner, + repository: { platform: 'github', repoFullName: 'acme/consistent' }, + }); + expect(edited).toBe(true); + expect(loaded?.reference.configurationRevision).toBe(1); + expect(loaded?.layer.envVars).toEqual({ VALUE: 'before' }); + const current = await loadEnvironmentForSession(db, { + owner, + repository: { platform: 'github', repoFullName: 'acme/consistent' }, + }); + expect(current?.reference.configurationRevision).toBe(2); + expect(current?.layer.envVars).toEqual({ VALUE: 'after' }); + }); + test('returns vars, envelopes, ordered commands, and size for the repository', async () => { const user = await insertTestUser(); await createEnv({ @@ -791,7 +872,7 @@ describe('repository environment service', () => { test('matches GitLab by the canonical clone URL and Bitbucket by UUID pair', async () => { const user = await insertTestUser(); const owner = userOwner(user.id); - await createEnv({ + const gitlabId = await createEnv({ owner, createdByUserId: user.id, name: 'gitlab', @@ -800,7 +881,7 @@ describe('repository environment service', () => { gitlabCloneUrl: 'https://gitlab.com/group/project.git', commands: ['gitlab-setup'], }); - await createEnv({ + const bitbucketId = await createEnv({ owner, createdByUserId: user.id, name: 'bitbucket', @@ -815,6 +896,7 @@ describe('repository environment service', () => { repository: { platform: 'gitlab', cloneUrl: 'https://gitlab.com/group/project.git' }, }); expect(gitlab?.layer.setupCommands).toEqual(['gitlab-setup']); + expect(gitlab?.reference.environmentId).toBe(gitlabId); await expect( loadEnvironmentForSession(db, { owner, @@ -831,6 +913,7 @@ describe('repository environment service', () => { repositoryUuid: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb', }, }); + expect(bitbucket?.reference.environmentId).toBe(bitbucketId); expect(bitbucket?.layer.setupCommands).toEqual([]); await expect( loadEnvironmentForSession(db, { @@ -845,6 +928,119 @@ describe('repository environment service', () => { }); }); + describe('configuration revision', () => { + async function createRevisionEnv(userId: string): Promise { + return createEnv({ + owner: userOwner(userId), + createdByUserId: userId, + name: 'CI', + platform: 'github', + repoFullName: 'acme/widgets', + plainVars: { PLAIN: 'one' }, + secrets: { TOKEN: 'secret-one' }, + commands: ['install'], + }); + } + + test('starts at 1 and keeps the revision for a rename or identical writes', async () => { + const user = await insertTestUser(); + const id = await createRevisionEnv(user.id); + const owner = userOwner(user.id); + expect((await getEnvironment(db, id, owner)).configurationRevision).toBe(1); + + await expect(updateEnv({ owner, environmentId: id, name: 'Renamed' })).resolves.toBe(1); + await expect( + updateEnv({ + owner, + environmentId: id, + plainVars: { PLAIN: 'one' }, + commands: ['install'], + secrets: [], + }) + ).resolves.toBe(1); + await expect( + updateEnv({ owner, environmentId: id, secrets: [{ key: 'ABSENT', delete: true }] }) + ).resolves.toBe(1); + expect((await getEnvironment(db, id, owner)).configurationRevision).toBe(1); + }); + + test('advances for command, plaintext variable and secret changes', async () => { + const user = await insertTestUser(); + const id = await createRevisionEnv(user.id); + const owner = userOwner(user.id); + + await expect( + updateEnv({ owner, environmentId: id, commands: ['install\nbuild'] }) + ).resolves.toBe(2); + await expect( + updateEnv({ owner, environmentId: id, plainVars: { PLAIN: 'two' } }) + ).resolves.toBe(3); + // A submitted replacement counts even when its value is the same: the + // stored value is encrypted and is never compared. + await expect( + updateEnv({ owner, environmentId: id, secrets: [{ key: 'TOKEN', value: 'secret-one' }] }) + ).resolves.toBe(4); + await expect( + updateEnv({ owner, environmentId: id, secrets: [{ key: 'TOKEN', delete: true }] }) + ).resolves.toBe(5); + const summaries = await listEnvironmentsForOwner(db, owner); + expect(summaries.find(summary => summary.id === id)?.configurationRevision).toBe(5); + }); + + test('reports the admitted reference and reads build inputs consistently', async () => { + const user = await insertTestUser(); + const id = await createRevisionEnv(user.id); + const owner = userOwner(user.id); + await updateEnv({ owner, environmentId: id, commands: ['install\nbuild'] }); + + const reference = ( + await loadEnvironmentForSession(db, { + owner, + repository: { platform: 'github', repoFullName: 'Acme/Widgets' }, + }) + )?.reference; + expect(reference).toEqual({ + environmentId: id, + owner, + configurationRevision: 2, + repository: { platform: 'github', repoFullName: 'acme/widgets' }, + }); + await expect(readEnvironmentReference(db, id)).resolves.toEqual(reference); + + const input = await loadEnvironmentBuildInput(db, id); + expect(input).toMatchObject({ + reference, + repoFullName: 'acme/widgets', + commands: ['install\nbuild'], + plainVars: { PLAIN: 'one' }, + }); + expect(Object.keys(input?.encryptedSecrets ?? {})).toEqual(['TOKEN']); + expect(JSON.stringify(input)).not.toContain('secret-one'); + }); + + test('reads nothing for a deleted environment or a deleted organization', async () => { + const user = await insertTestUser(); + const id = await createRevisionEnv(user.id); + await deleteEnvironment(db, id, userOwner(user.id)); + await expect(readEnvironmentReference(db, id)).resolves.toBeNull(); + await expect(loadEnvironmentBuildInput(db, id)).resolves.toBeNull(); + + const organization = await createTestOrganization('Revision org', user.id, 0); + const orgId = await createEnv({ + owner: { type: 'organization', id: organization.id }, + createdByUserId: user.id, + name: 'org', + platform: 'github', + repoFullName: 'acme/widgets', + }); + await db + .update(organizations) + .set({ deleted_at: new Date().toISOString() }) + .where(eq(organizations.id, organization.id)); + await expect(readEnvironmentReference(db, orgId)).resolves.toBeNull(); + }); + }); + describe('write limits', () => { test('rejects too many combined variables and secrets', async () => { const user = await insertTestUser(); diff --git a/packages/cloud-agent-profile/src/environment-contracts.ts b/packages/cloud-agent-profile/src/environment-contracts.ts index 4c2dc6e9ac5..4c99b68becd 100644 --- a/packages/cloud-agent-profile/src/environment-contracts.ts +++ b/packages/cloud-agent-profile/src/environment-contracts.ts @@ -119,13 +119,42 @@ export type EnvironmentSessionLayer = { setupCommands?: string[]; }; +/** + * The authorized identity of the environment a session admitted: who owns it, + * which repository it belongs to, and the configuration revision whose values + * the session merged. It travels beside the merged configuration so snapshot + * selection can check ownership, repository and revision without re-reading + * the environment. + */ +export type EnvironmentReference = { + environmentId: string; + owner: EnvironmentOwner; + configurationRevision: number; + repository: EnvironmentRepositoryIdentity; +}; + /** What session preflight loads for the environment of a session's repository. */ export type EnvironmentForSession = { layer: EnvironmentSessionLayer; + reference: EnvironmentReference; /** Saved allocation key, unvalidated; the Worker checks eligibility. */ sandboxAllocation: string | null; }; +/** + * The immutable inputs of one environment build, read consistently from the + * environment row and its variable rows. Secrets stay encrypted envelopes until + * the builder needs them. `repoFullName` is the display path the clone URL is + * built from for GitHub and Bitbucket. + */ +export type EnvironmentBuildInput = { + reference: EnvironmentReference; + repoFullName: string; + commands: string[]; + plainVars: Record; + encryptedSecrets: Record; +}; + export type EnvironmentPlainVarResponse = { key: string; value: string; @@ -145,6 +174,7 @@ export type EnvironmentResponse = { updatedAt: string; plainVars: EnvironmentPlainVarResponse[]; commands: string[]; + configurationRevision: number; sandboxAllocation: string | null; /** Secret key names only. Values and envelopes are never returned. */ secretKeys: string[]; @@ -163,6 +193,7 @@ export type EnvironmentSummary = { plainVarCount: number; secretCount: number; commandCount: number; + configurationRevision: number; sandboxAllocation: string | null; }; diff --git a/packages/cloud-agent-profile/src/environment-service.ts b/packages/cloud-agent-profile/src/environment-service.ts index fcfb5835866..724c03ce1b6 100644 --- a/packages/cloud-agent-profile/src/environment-service.ts +++ b/packages/cloud-agent-profile/src/environment-service.ts @@ -5,7 +5,7 @@ import { organizations, type CloudAgentEnvironment, } from '@kilocode/db/schema'; -import { and, count, eq, inArray, isNull } from 'drizzle-orm'; +import { and, count, eq, inArray, isNull, sql, type SQL } from 'drizzle-orm'; import { TRPCError } from '@trpc/server'; import { encryptWithPublicKey } from '@kilocode/encryption'; import type { EnvironmentPlatform } from './environment-contracts'; @@ -21,13 +21,14 @@ import { validateEnvironmentVarCount, normalizeRepoFullName, normalizeBitbucketUuid, + type EnvironmentBuildInput, type EnvironmentCreateInput, type EnvironmentForSession, type EnvironmentOwner, + type EnvironmentReference, type EnvironmentRepositoryIdentity, type EnvironmentResponse, type EnvironmentSecretChange, - type EnvironmentSessionLayer, type EnvironmentSummary, type EnvironmentUpdateInput, } from './environment-contracts'; @@ -159,6 +160,7 @@ function toResponseColumns(environment: CloudAgentEnvironment): { bitbucketRepositoryUuid: string | null; createdAt: string; updatedAt: string; + configurationRevision: number; sandboxAllocation: string | null; } { return { @@ -171,10 +173,69 @@ function toResponseColumns(environment: CloudAgentEnvironment): { bitbucketRepositoryUuid: environment.bitbucket_repository_uuid, createdAt: environment.created_at, updatedAt: environment.updated_at, + configurationRevision: environment.configuration_revision, sandboxAllocation: environment.sandbox_allocation, }; } +function ownerOf(environment: CloudAgentEnvironment): EnvironmentOwner { + if (environment.owned_by_organization_id !== null) { + return { type: 'organization', id: environment.owned_by_organization_id }; + } + if (environment.owned_by_user_id !== null) { + return { type: 'user', id: environment.owned_by_user_id }; + } + throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Environment has no owner' }); +} + +/** The stored repository identity, in the shape every comparison uses. */ +function repositoryIdentityOf(environment: CloudAgentEnvironment): EnvironmentRepositoryIdentity { + switch (environment.platform) { + case 'github': + return { platform: 'github', repoFullName: environment.repo_full_name }; + case 'gitlab': + if (environment.gitlab_clone_url === null) break; + return { platform: 'gitlab', cloneUrl: environment.gitlab_clone_url }; + case 'bitbucket': + if ( + environment.bitbucket_workspace_uuid === null || + environment.bitbucket_repository_uuid === null + ) { + break; + } + return { + platform: 'bitbucket', + workspaceUuid: environment.bitbucket_workspace_uuid, + repositoryUuid: environment.bitbucket_repository_uuid, + }; + } + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Environment repository identity is incomplete', + }); +} + +function referenceOf(environment: CloudAgentEnvironment): EnvironmentReference { + return { + environmentId: environment.id, + owner: ownerOf(environment), + configurationRevision: environment.configuration_revision, + repository: repositoryIdentityOf(environment), + }; +} + +function sameStrings(left: readonly string[], right: readonly string[]): boolean { + return left.length === right.length && left.every((value, index) => value === right[index]); +} + +function sameRecord(left: Record, right: Record): boolean { + const keys = Object.keys(left); + return ( + keys.length === Object.keys(right).length && + keys.every(key => Object.hasOwn(right, key) && right[key] === left[key]) + ); +} + async function replacePlainVars( tx: EnvironmentTransaction, environmentId: string, @@ -196,15 +257,17 @@ async function replacePlainVars( } } +/** Apply secret changes; returns whether any stored secret was replaced or removed. */ async function applySecretChanges( tx: EnvironmentTransaction, environmentId: string, publicKeyBase64: string, changes: EnvironmentSecretChange[] -): Promise { +): Promise { + let changed = false; for (const change of changes) { if ('delete' in change) { - await tx + const deleted = await tx .delete(cloud_agent_environment_vars) .where( and( @@ -212,9 +275,12 @@ async function applySecretChanges( eq(cloud_agent_environment_vars.key, change.key), eq(cloud_agent_environment_vars.is_secret, true) ) - ); + ) + .returning({ id: cloud_agent_environment_vars.id }); + if (deleted.length > 0) changed = true; continue; } + changed = true; const encryptedValue = encryptValue(publicKeyBase64, change.value); await tx @@ -234,6 +300,7 @@ async function applySecretChanges( }, }); } + return changed; } async function countsFor( @@ -335,7 +402,7 @@ export async function updateEnvironment( tx: EnvironmentTransaction, publicKeyBase64: string, input: EnvironmentUpdateInput -): Promise { +): Promise<{ configurationRevision: number }> { const name = input.name === undefined ? undefined : validateEnvironmentName(input.name); const plainVars = input.plainVars === undefined ? undefined : validateEnvironmentPlainVars(input.plainVars); @@ -363,6 +430,7 @@ export async function updateEnvironment( throw new TRPCError({ code: 'NOT_FOUND', message: 'Environment not found' }); } + // Name and allocation are not build inputs; they never advance the revision. if (name !== undefined || input.sandboxAllocation !== undefined) { await tx .update(cloud_agent_environments) @@ -382,6 +450,24 @@ export async function updateEnvironment( } } + // Build inputs change when the submitted commands or plaintext variables + // differ from the stored ones, or when any secret is replaced or removed. A + // submitted secret value is never compared: the stored value is encrypted. + let buildInputsChanged = commands !== undefined && !sameStrings(commands, environment.commands); + if (plainVars !== undefined && !buildInputsChanged) { + const storedPlainRows = await tx + .select({ key: cloud_agent_environment_vars.key, value: cloud_agent_environment_vars.value }) + .from(cloud_agent_environment_vars) + .where( + and( + eq(cloud_agent_environment_vars.environment_id, input.environmentId), + eq(cloud_agent_environment_vars.is_secret, false) + ) + ); + const storedPlainVars = Object.fromEntries(storedPlainRows.map(row => [row.key, row.value])); + buildInputsChanged = !sameRecord(plainVars, storedPlainVars); + } + if (plainVars !== undefined) { const existingSecretRows = await tx .select({ key: cloud_agent_environment_vars.key }) @@ -404,8 +490,10 @@ export async function updateEnvironment( // Secret changes run before the plaintext replacement so a deleted secret // frees its key for a plaintext variable of the same name. - if (secretChanges !== undefined) { - await applySecretChanges(tx, input.environmentId, publicKeyBase64, secretChanges); + if (secretChanges !== undefined && secretChanges.length > 0) { + if (await applySecretChanges(tx, input.environmentId, publicKeyBase64, secretChanges)) { + buildInputsChanged = true; + } } if (plainVars !== undefined) { @@ -424,6 +512,19 @@ export async function updateEnvironment( .from(cloud_agent_environment_vars) .where(eq(cloud_agent_environment_vars.environment_id, input.environmentId)); validateEnvironmentVarCount(rows.length); + + if (!buildInputsChanged) return { configurationRevision: environment.configuration_revision }; + // The row is locked FOR UPDATE above, so the increment is serialized with + // every other edit of this environment. + const [updated] = await tx + .update(cloud_agent_environments) + .set({ configuration_revision: sql`${cloud_agent_environments.configuration_revision} + 1` }) + .where(eq(cloud_agent_environments.id, input.environmentId)) + .returning({ configurationRevision: cloud_agent_environments.configuration_revision }); + if (!updated) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Environment not found' }); + } + return { configurationRevision: updated.configurationRevision }; } export async function deleteEnvironment( @@ -543,16 +644,40 @@ export async function listEnvironmentsForOwner( return summarizeEnvironments(db, environments); } -/** - * Load the environment of a session's repository: the primitives the session - * merges and the saved allocation. Null when the owner has no environment for - * the repository or the owning organization is soft-deleted. An owner has at - * most one environment per repository. - */ -export async function loadEnvironmentForSession( - db: WorkerDb, - input: { owner: EnvironmentOwner; repository: EnvironmentRepositoryIdentity } -): Promise { +/** Split variable rows into plaintext values and parsed secret envelopes. */ +function splitVariables(vars: ReadonlyArray<{ key: string; value: string; isSecret: boolean }>): { + plainVars: Record; + encryptedSecrets: EnvironmentBuildInput['encryptedSecrets']; +} { + const plainVars: Record = {}; + const encryptedSecrets: EnvironmentBuildInput['encryptedSecrets'] = {}; + for (const variable of vars) { + if (variable.isSecret) { + encryptedSecrets[variable.key] = encryptedEnvelopeSchema.parse(JSON.parse(variable.value)); + } else { + plainVars[variable.key] = variable.value; + } + } + return { plainVars, encryptedSecrets }; +} + +async function readVariables(db: Pick, environmentId: string) { + return db + .select({ + key: cloud_agent_environment_vars.key, + value: cloud_agent_environment_vars.value, + isSecret: cloud_agent_environment_vars.is_secret, + }) + .from(cloud_agent_environment_vars) + .where(eq(cloud_agent_environment_vars.environment_id, environmentId)) + .orderBy(cloud_agent_environment_vars.key); +} + +/** The matching environment row when its owning organization is active. */ +async function readActiveEnvironment( + db: Pick, + condition: SQL | undefined +): Promise { const [row] = await db .select({ environment: cloud_agent_environments }) .from(cloud_agent_environments) @@ -560,31 +685,91 @@ export async function loadEnvironmentForSession( organizations, eq(cloud_agent_environments.owned_by_organization_id, organizations.id) ) - .where(and(identityCondition(input.owner, input.repository), isNull(organizations.deleted_at))) + .where(and(condition, isNull(organizations.deleted_at))) .limit(1); - if (!row) return null; + return row?.environment ?? null; +} - const vars = await db - .select({ - key: cloud_agent_environment_vars.key, - value: cloud_agent_environment_vars.value, - isSecret: cloud_agent_environment_vars.is_secret, - }) - .from(cloud_agent_environment_vars) - .where(eq(cloud_agent_environment_vars.environment_id, row.environment.id)); +/** + * Load the environment of a session's repository: the primitives the session + * merges, the authorized reference that travels beside them, and the saved + * allocation. Null when the owner has no environment for the repository or the + * owning organization is soft-deleted. An owner has at most one environment + * per repository. + */ +export async function loadEnvironmentForSession( + db: WorkerDb, + input: { owner: EnvironmentOwner; repository: EnvironmentRepositoryIdentity } +): Promise { + return db.transaction( + async tx => { + const environment = await readActiveEnvironment( + tx, + identityCondition(input.owner, input.repository) + ); + if (!environment) return null; - const envVars: Record = {}; - const encryptedSecrets: EnvironmentSessionLayer['encryptedSecrets'] = {}; - for (const variable of vars) { - if (variable.isSecret) { - encryptedSecrets[variable.key] = encryptedEnvelopeSchema.parse(JSON.parse(variable.value)); - } else { - envVars[variable.key] = variable.value; - } - } + const { plainVars, encryptedSecrets } = splitVariables( + await readVariables(tx, environment.id) + ); + return { + layer: { + envVars: plainVars, + encryptedSecrets, + setupCommands: environment.commands, + }, + reference: referenceOf(environment), + sandboxAllocation: environment.sandbox_allocation, + }; + }, + { isolationLevel: 'repeatable read', accessMode: 'read only' } + ); +} - return { - layer: { envVars, encryptedSecrets, setupCommands: row.environment.commands }, - sandboxAllocation: row.environment.sandbox_allocation, - }; +/** + * The current owner, revision and repository of an environment, or null when + * it no longer exists or its owning organization is deleted. Build admission + * and publication recheck this before acting. + */ +export async function readEnvironmentReference( + db: WorkerDb, + environmentId: string +): Promise { + const environment = await readActiveEnvironment( + db, + eq(cloud_agent_environments.id, environmentId) + ); + return environment === null ? null : referenceOf(environment); +} + +/** + * Read one build's inputs consistently: the environment row and its variable + * rows come from one repeatable-read snapshot, so a concurrent edit cannot mix + * two revisions. Secrets stay encrypted envelopes. Null when the environment + * no longer exists. + */ +export async function loadEnvironmentBuildInput( + db: WorkerDb, + environmentId: string +): Promise { + return db.transaction( + async tx => { + const environment = await readActiveEnvironment( + tx, + eq(cloud_agent_environments.id, environmentId) + ); + if (environment === null) return null; + const { plainVars, encryptedSecrets } = splitVariables( + await readVariables(tx, environmentId) + ); + return { + reference: referenceOf(environment), + repoFullName: environment.repo_full_name, + commands: environment.commands, + plainVars, + encryptedSecrets, + }; + }, + { isolationLevel: 'repeatable read', accessMode: 'read only' } + ); } diff --git a/packages/cloud-agent-profile/src/index.ts b/packages/cloud-agent-profile/src/index.ts index 4732f032f61..29c3ca8edee 100644 --- a/packages/cloud-agent-profile/src/index.ts +++ b/packages/cloud-agent-profile/src/index.ts @@ -241,7 +241,9 @@ export { type EnvironmentSecretChange, type EnvironmentUpdateInput, type EnvironmentSessionLayer, + type EnvironmentReference, type EnvironmentForSession, + type EnvironmentBuildInput, type EnvironmentResponse, type EnvironmentSummary, } from './environment-contracts'; @@ -253,6 +255,8 @@ export { listEnvironments, listEnvironmentsForOwner, loadEnvironmentForSession, + loadEnvironmentBuildInput, + readEnvironmentReference, lockActiveOrganization, type EnvironmentTransaction, } from './environment-service'; diff --git a/packages/db/src/migrations/0302_loud_victor_mancha.sql b/packages/db/src/migrations/0302_loud_victor_mancha.sql new file mode 100644 index 00000000000..a30adcb38dc --- /dev/null +++ b/packages/db/src/migrations/0302_loud_victor_mancha.sql @@ -0,0 +1 @@ +ALTER TABLE "cloud_agent_environments" ADD COLUMN "configuration_revision" integer DEFAULT 1 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..38ac5056201 --- /dev/null +++ b/packages/db/src/migrations/meta/0302_snapshot.json @@ -0,0 +1,39378 @@ +{ + "id": "3ef93f9d-c2d8-4ef7-87ce-4876b6dba5de", + "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": "'{}'" + }, + "configuration_revision": { + "name": "configuration_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "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.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..fcc52a3eda0 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": 1792878599267, + "tag": "0302_loud_victor_mancha", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index b2f32f9b595..4c3bdedd3c3 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -7985,8 +7985,11 @@ export const cloud_agent_environments = pgTable( // Ordered setup commands run after clone. Array order is the run order. commands: text().array().default([]).notNull(), + // Monotonic build-input revision. Advances when commands, plaintext + // variables or secrets change; a rename or an identical write keeps it. + configuration_revision: integer().default(1).notNull(), // Saved sandbox allocation key for this repository's sessions; null uses the - // owner's default. + // owner's default. Not a build input, so it never advances the revision. sandbox_allocation: text(), created_at: timestamp({ withTimezone: true, mode: 'string' }).defaultNow().notNull(), diff --git a/services/cloud-agent-next/.dev.vars.example b/services/cloud-agent-next/.dev.vars.example index 5d8efc10e8e..9fa241a75d0 100644 --- a/services/cloud-agent-next/.dev.vars.example +++ b/services/cloud-agent-next/.dev.vars.example @@ -64,12 +64,6 @@ RUNTIME_ISOLATION_ENABLED=true CREDENTIAL_CONTAINMENT_ENABLED=false -# Container repo snapshot IDs (optional) -# Comma-separated user or org IDs whose isolated control-plane containers start from a saved -# repository snapshot, or `*` for all. Empty disables it. Needs the REPO_SNAPSHOTS KV -# 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. @@ -91,6 +85,8 @@ VERCEL_SANDBOX_ORG_IDS= # Cloudflare containers enrollment (optional). Comma-separated organization IDs, or `*` for # every organization and personal accounts. Empty is off. CLOUDFLARE_CONTAINERS_ORG_IDS= +# Explicit organization IDs allowed to build and restore snapshots. Empty is off; no wildcard. +ENVIRONMENT_SNAPSHOT_ORG_IDS= # GitHub App credentials for git commit attribution # These identify commits as coming from the Kilo Code GitHub App diff --git a/services/cloud-agent-next/docs/control-plane.md b/services/cloud-agent-next/docs/control-plane.md index 1f9c211af61..b739f5f510e 100644 --- a/services/cloud-agent-next/docs/control-plane.md +++ b/services/cloud-agent-next/docs/control-plane.md @@ -476,59 +476,69 @@ These keep their current owners and evidence; the rewrite ports them, it does no - Worktree deletion reports an incomplete, retryable result when the provider has not confirmed the stop or cleanup (Shared Worktrees rule 13). -### Repository snapshots - -Scope: `workspace_*` routes on `cloudflare-containers` whose directory is the constant isolated -path `/workspace/app`. A snapshot holds one repository at one path, so any other route has no key. - -- **Key.** When a route is first prepared, the Sandbox DO hashes the owner, the repository URL and - the setup commands with HMAC-SHA256 under a Worker secret. Env is not part of the key: setup - re-runs on every start and rewrites what it derives from the env. The setup commands join the - hash so a snapshot is reused only by a route with the same setup; a different setup for one - repository gets its own key. It stores the digest on the route (`repo_key`) and keeps it across - attempts. No key is computed, and no snapshot used, when the owner is not enrolled - (`CONTAINER_REPO_SNAPSHOT_IDS`), the route has no repository, the provider cannot capture, or the - secret is missing. Scope is per user; per org is a change to that one field. The Sandbox DO - adds the allocation's startup mode to the key for `launch` and capture, so a snapshot captured - in one mode is never restored in the other. -- **Start.** `launch` receives the key of the routes waiting for the allocation: exactly one key, - else none. `SandboxContainers` appends the image, and starts from the - repository snapshot in the `REPO_SNAPSHOTS` KV index when it has one for that digest, else from - the image. `launch` returns `startSource`. A failed repository start retains the index entry - and asks the Sandbox DO to clean up the failed container and launch the image for the same - allocation. Cleanup and fallback only run while that allocation is still creating; a wrapper - that already connected is never destroyed by fallback. Before cleanup, the Sandbox DO rotates - the launch credential so a late wrapper from the failed snapshot cannot connect. The snapshot attempt, cleanup and image fallback share the original - launch deadline. No separate half-budget reservation exists. - Each later sandbox start retries the snapshot once, up to ten failed starts per snapshot ID - in that sandbox's durable record. After that it uses the image; a replacement snapshot gets - its own failure budget. Failure never deletes or changes the shared snapshot index. -- **First start only.** The Sandbox DO remembers how the previous allocation started and whether its - wrapper connected. When a repository start never connected (for example the first-connect timeout - stopped it), the next launch skips the snapshot and starts from the image without deleting - the entry. A completed image fallback records `image`, so the next start tries the retained - snapshot again unless its failure budget is exhausted. -- **Capture.** `session.prepare` carries `capture: true` when the route has a key and the provider - can capture. The key names the setup commands as well as the user and the repository, because - capture runs after setup: a snapshot built with one setup is never adopted by a route with - another. The - wrapper decides whether to capture (a fresh clone, or an adopted snapshot that is - due for a refresh) and then sends `workspace.capture` after setup, with origin bare. The Sandbox DO - calls the provider off its serial queue, bounded at 5 min 5 s, and answers `workspace.captured` - (`ok: false` at once when nothing can be saved). `SandboxContainers` snapshots the running - container (bounded at 5 min), and writes `{ snapshotId, commit }` to the index with a 10-day TTL, - which only expires a snapshot nobody uses (the platform keeps one for 30 days). - It does not take its operation queue, so a stop or launch is never delayed by a capture. The - snapshot is published only while that allocation is still the running one. - Each capture logs one line with its `outcome` (`stored`, `index_unavailable`, `abandoned` or - `failed`) and the `durationMs` of the platform snapshot, to tune the bound from real durations. -- **No session snapshots.** A stop destroys the container without a snapshot. A restarted session - uses the repository snapshot, then restores its Kilo history from session-ingest. -- **Image tie.** A deploy that changes the image changes the index key, so the next start per key is - cold. The platform keeps an unused snapshot for 30 days; there is no list or delete API. The - runtime-free image changes only when its own inputs change, so a runtime-only deploy keeps - `downloaded` snapshots. A restored `downloaded` sandbox runs the bootstrap first; the captured - runtime never starts. +### Environment snapshots + +Scope: `workspace_*` routes on `cloudflare-containers` whose directory is the isolated path +`/workspace/app` and whose session was admitted with a repository environment. Sessions never +capture. One `EnvironmentBuild` Durable Object per environment ID builds, publishes and serves the +snapshot (`src/environment-build/`); see `.specs/cloud-agent-session.md` "Environment snapshots". + +- **Reference.** For an origin under the `include-web-defaults` profile policy, session preflight + looks up the owner's one environment for the repository and stores + `{ environmentId, owner, repository, configurationRevision }` on the session metadata and on the + route (`routes.environment`). Its saved sandbox allocation becomes the first saved candidate, + ahead of the repository override and the owner default. A client-supplied reference or + allocation is dropped. Worktree chats inherit the reference. +- **Candidate.** At `runCreate` the Sandbox DO uses a snapshot only when every preparing route waits + at `/workspace/app` with the same environment, revision and owner, and a route carries the + credential user (`environmentLaunchCandidate`). Any other allocation starts from the image. +- **Progress.** The decision is its own preparation step, `environment_snapshot`, so its detail + stays in the completed timeline after `sandbox_start` reports. +- **Lookup.** The Sandbox DO calls `resolveSnapshot` once, bounded at 15 s and by the launch + deadline, outside its serial queue. A timeout, a miss or an unavailable coordinator is an ordinary + image start; it never waits for a build. The coordinator answers with the published descriptor + when it is eligible (same scope, the current base image and runtime ABI, revision not newer + than the session's, under the 29-day lifetime) and with the replacement admission: `admitted`, `active`, `not_needed` or `skipped` + (backoff, container limit, another build). An older revision or an age-stale snapshot (4 days) + stays usable while its replacement builds. A descriptor for another base image or runtime ABI + (after a deploy that changes the image) is not used and admits a replacement; the coordinator + reads the current image from the builder's container DO. +- **Start.** `launch` receives `snapshot: { snapshotId, baseImage }`. `SandboxContainers` refuses a + descriptor for another base image (`image_mismatch`) and otherwise restores it. `launch` returns + `startSource` (`repository` or `image`). The progress detail names the snapshot capture date and time in UTC, the + changed environment settings and the real admission outcome. +- **Fallback.** A failed snapshot start cleans up that container, rotates the launch credential so a + late wrapper cannot connect, and launches the image for the same allocation within the original + deadline. When the previous snapshot start never connected or its preparation failed, the next + launch uses the image once (`previousSnapshotStartFailed`); a completed image start clears this. + A failure never changes the published snapshot. Each native container coordinator retains up to 16 failed snapshot IDs. + After three failed restores of one snapshot, it starts from the image until a replacement is offered. +- **No session capture.** A wrapper that predates this change may still send `workspace.capture`; + the Sandbox DO answers `workspace.captured` with `ok: false` at once. The `REPO_SNAPSHOTS` index, + repository keys and route `repo_key` are removed. +- **Builder.** A build runs in its own `sbx-` container DO named `environment-build:v1:` + (`standard-4` when setup commands exist), billed and capacity-checked like an isolated session. + Its main process is `kilocode-environment-builder.js` under the runtime Bun, not the supervisor. + The container starts without the runner endpoint or credential in its env, because a snapshot + keeps the start configuration; the container DO writes them to + `/run/kilocode-environment-build/runner.json` by exec, and the builder deletes the file once + read. It reads its input from `/environment-build/:envId/:buildId/input` with that per-build + HMAC credential (input only while the build is `preparing`; no route accepts it after the build + ends), reports events and its result + over the same authenticated routes, clones the default + branch into `/workspace/app`, runs setup with the directory HOME, removes credentials and + reflogs, and writes the workspace stamp. The coordinator records the capture as issued before it + calls the platform, captures, publishes when the build revision is still current, and stops the + builder. Heartbeats from reports renew the build lease; a builder silent for 90 s is lost. +- **Lifetime.** The platform keeps an unused snapshot for 30 days and has no list or delete API in + this release, so a superseded snapshot is unreferenced rather than deleted. +- **Runtime selection.** Builders and sessions use the deployment's explicit startup selection. + Downloaded mode uses the current runtime installer and its named builder or supervisor target, + including on snapshot restore. The installed control bundle includes the builder. A preinstalled + launch rejects snapshots captured with downloaded aliases; a downloaded launch can install a + newer compatible runtime over a snapshot. The artifact read grant travels in the installer + command and expires at the startup deadline. A restored session supplies a new command and its + own credentials. Native snapshot retention of start configuration still needs deployed validation. ## 7. Wrapper @@ -590,7 +600,6 @@ The wrapper owns the step timeouts and retries: | Use a prepared repository (`restore`) | 2 min | Network errors: 3 attempts; any other failure falls back to a clone | | Checkout, branch restore | In the clone budget | No | | Setup commands | Current per-command limits | No; a failure fails preparation | -| Save the repository (`snapshot`) | 5 min 10 s wait | No; a failure is logged and preparation continues | | Kilo runtime start | 2 min | 1 retry | | Kilo session: use the one Kilo has on disk; if missing (new sandbox), restore from the snapshot; else create | 2 min | 1 retry | @@ -611,30 +620,34 @@ options are command arguments, not Git config. #### Workspace stamp -A prepared workspace carries `.git/kilo-workspace.json` (`{ allocationId, commit, capturedAt, -generation }`; beside the directory when there is no repository). It replaces the boolean bootstrap marker. At -`session.prepare` the wrapper decides from the filesystem alone: +A prepared workspace carries `.git/kilo-workspace.json` (`{ allocationId, commit }`; beside the +directory when there is no repository). The reader also accepts the older `capturedAt` and +`generation` fields. At `session.prepare` the wrapper decides from the filesystem alone: | Found | Meaning | Work | |---|---|---| -| No `.git` | Image start | Clone, checkout, setup, stamp, then capture when asked | -| `.git`, no stamp | A preparation that did not finish | Reuse the clone: checkout, setup, stamp. Never captured. | +| No `.git` | Image start | Clone, checkout, setup, stamp | +| `.git`, no stamp | A preparation that did not finish | Reuse the clone: checkout, setup, stamp | | Stamp from this allocation | A sibling session or a wrapper restart | Nothing | -| Stamp from another allocation | A restored repository snapshot | **Adopt** | +| Stamp from another allocation | A restored environment snapshot | **Adopt** | Adopt makes the snapshot equal to a fresh clone, then runs the ordinary steps: 1. Point `origin` at this route's credential. 2. `fetch --prune`, then refresh the remote default branch. 3. Detach at the remote default branch, so a new branch starts from its tip. -4. Delete every local branch. The captured session's branch, or this session's own from an earlier - capture, would otherwise shadow a newer `origin/`. +4. Delete every local branch. The builder's default branch would otherwise shadow a newer + `origin/`. 5. Check out this route's branch (working branch, explicit branch or review ref) and set the git author, as after a clone. 6. Run the setup commands, then write the stamp. -Any failure empties the directory and clones. The Kilo session step is unchanged: the snapshot has -no Kilo home, so nothing stale shadows the restore from session-ingest. `session.ready` reports +Any failure empties the directory and clones. The Kilo session step is unchanged: the builder +starts no Kilo runtime, so nothing stale shadows the restore from session-ingest. HOME of a +`worktree` runtime is `/home/`, the same path the builder uses, so caches that +setup wrote (`~/.cache`, toolchains) are present after restore. An allocation that keeps +`runtimeIsolation: per-session` keeps its per-session HOME under `$TMPDIR/kilo-worktrees`; the log +uploader reads both roots. `session.ready` reports `workspace: 'cloned' | 'same' | 'adopted'`. Before starting Kilo, materialize the session's profile skills (including companion files) into @@ -646,24 +659,6 @@ Preparation replaces stale profile artifacts from restored homes, and credential the same profile config path. Read-only Bitbucket reviews withhold these collections, matching the legacy profile restrictions. -#### Capture - -A route captures only when `session.prepare` carries `capture: true`, and then when it cloned -(generation 0) or when the snapshot it adopted is due for a **refresh**: captured 4 days ago or -more. A refresh captures the adopted workspace, so it costs the incremental setup rather than a cold -clone and install. A snapshot that is not due keeps its `capturedAt` and generation in the new -stamp, so its age keeps counting. Each capture stacks on the last, so after 4 refreshes a due -snapshot is **rebuilt** instead: the wrapper does not adopt it, empties the directory, clones, and -captures a new generation 0. That bounds what stacked captures accumulate (deleted files, untracked -leftovers). Concurrent sessions that adopt a due snapshot each capture, and the last write wins. -After -setup the wrapper writes the stamp (before the snapshot, so a restored container sees another -allocation's stamp), sets `origin` to the bare URL, clears the reflogs and `FETCH_HEAD`, sends -`workspace.capture` and waits for `workspace.captured`. It then restores the authenticated URL, -whatever the outcome, and continues to the Kilo runtime. A snapshot therefore holds no agent edits, -no Kilo home and no git credential. Process env never reaches the disk. Capture runs after setup, so -the key names the setup commands too: a snapshot is reused only by a route with the same setup. - ### Prompts and turn outcome - `session.prompt` is submitted to Kilo with `messageID` = `messageId`, in arrival order, also while @@ -880,10 +875,10 @@ them through one development-only override. | Sandbox DO | Artifact grant | `createDeadlineAt` plus first connect | Downloads are rejected | | Bootstrap | Install | Until the first-connect deadline, rendered by the Worker | Report `deadline_exceeded` and exit | | Sandbox DO | Provider stop | Existing ladder | Log unconfirmed stop; routing state `stopped` | -| Sandbox DO | Repository capture call | 5 min 5 s (container DO: 5 min) | Answer `ok: false`; preparation continues | +| Sandbox DO | Environment snapshot lookup | 15 s, capped by the launch deadline | Start from the image | | Wrapper | Preparation steps | Section 7 | Route `failed` with the step reason | -| Wrapper | Adopt a repository snapshot | 2 min | Empty the directory and clone | -| Wrapper | Wait for a capture | 5 min 10 s | Continue without a snapshot | +| Wrapper | Adopt an environment snapshot | 2 min | Empty the directory and clone | +| Environment build DO | One build | 30 min including capture | Build `failed` (`deadline`); stop the builder | | Wrapper | SSE silence | 30 s | Health request | | Wrapper | Kilo health request | 5 s | Restart Kilo | | Wrapper | SSE reconnects | 6 in 2 min | Restart Kilo | @@ -907,8 +902,7 @@ them through one development-only override. | Runtime install fails transiently, or the base is incompatible | Bootstrap report | Stop; new allocation within the route deadline, from the image after a restore | Queued fail at the deadline | | Startup report lost | Sandbox DO, 5 min | Same as "Wrapper never connects" | Queued fail at the deadline | | Clone network error | Wrapper | 3 attempts | Queued fail (`workspace_setup_failed`) | -| Repository snapshot cannot be used (start, adopt) | Container DO, wrapper | Start from the image, or clone | None; a cold start | -| Repository capture fails or times out | Sandbox DO, wrapper | Continue without a snapshot | None | +| Environment snapshot cannot be used (lookup, start, adopt) | Sandbox DO, container DO, wrapper | Start from the image, or clone | None; a cold start | | Setup command fails | Wrapper | None | Queued fail with the command output visible | | Socket drops, wrapper returns | Sandbox DO | Wrapper reconnects | None | | Socket down 90 s | Sandbox DO | Stop the sandbox | Accepted fail (`connection_lost`); queued re-prepare | @@ -1039,8 +1033,8 @@ The Sandbox DO stores one runtime contract per allocation in `allocation_runtime Registration retains `runtimeIsolation` for compatibility. The Sandbox removes it only from frames for a negotiated shared allocation. Updated wrappers still honor it for legacy allocations. -This preserves old runtime and HOME identities. Shared HOME uses -`sha256(scopeId + "\0" + directory)` under the wrapper home root. +This preserves old runtime and HOME identities. A `worktree` runtime HOME is +`/home/`, shared with the environment builder. Deploy in this order: @@ -1081,8 +1075,8 @@ differs from the warm runtime's fails with `Kilo worktree MCP configuration mism running with another session's servers. Sandbox DO ↔ wrapper (WebSocket frames): `hello`, `welcome`, `shutdown`, `heartbeat`, `heartbeat_ack`, -`session.prepare`, `session.progress`, `session.ready`, `session.failed`, `workspace.capture`, -`workspace.captured`, `session.credentials`, `session.prompt`, `session.abort`, `session.answer`, `session.release`, `session.events`, +`session.prepare`, `session.progress`, `session.ready`, `session.failed`, `workspace.capture` and +`workspace.captured` (legacy wrappers only; always answered `ok: false`), `session.credentials`, `session.prompt`, `session.abort`, `session.answer`, `session.release`, `session.events`, `session.outcome`, `events_dropped`, terminal control requests, worktree-change requests, worktree-deletion requests (`worktree.prepareDeletion`, `worktree.delete`; both answer with `worktree.result`). Terminal bytes use the existing wrapper-to-Session-DO terminal socket. diff --git a/services/cloud-agent-next/drizzle.environment-build.config.ts b/services/cloud-agent-next/drizzle.environment-build.config.ts new file mode 100644 index 00000000000..43fbcc2e385 --- /dev/null +++ b/services/cloud-agent-next/drizzle.environment-build.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'drizzle-kit'; + +// The EnvironmentBuild Durable Object owns its own SQLite schema and migrations. +export default defineConfig({ + out: './src/environment-build/drizzle', + schema: './src/environment-build/sqlite-schema.ts', + dialect: 'sqlite', + driver: 'durable-sqlite', +}); diff --git a/services/cloud-agent-next/scripts/runtime-manifest.test.ts b/services/cloud-agent-next/scripts/runtime-manifest.test.ts index c2c2d1229c9..a2f8b434940 100644 --- a/services/cloud-agent-next/scripts/runtime-manifest.test.ts +++ b/services/cloud-agent-next/scripts/runtime-manifest.test.ts @@ -30,6 +30,7 @@ describe('runtime distribution contract', () => { 'src/bitbucket-review-cli.ts', 'src/github-review-publish-mcp.ts', 'src/control-plane/main.ts', + 'src/environment-builder/main.ts', ]); expect(new Set(WRAPPER_BUNDLES.map(bundle => bundle.installName)).size).toBe( WRAPPER_BUNDLES.length @@ -43,6 +44,7 @@ describe('runtime distribution contract', () => { 'bb', 'github-review-publish-mcp', 'kilocode-control-plane-wrapper.js', + 'kilocode-environment-builder.js', 'kilocode-control-plane-supervisor.sh', 'kilo-git-credential', ]); diff --git a/services/cloud-agent-next/src/container-usage-context.ts b/services/cloud-agent-next/src/container-usage-context.ts index 24ac043b5f5..00a625b9f40 100644 --- a/services/cloud-agent-next/src/container-usage-context.ts +++ b/services/cloud-agent-next/src/container-usage-context.ts @@ -249,6 +249,7 @@ const KNOWN_ORIGINS = new Set([ 'cloud-agent', 'cloud-agent-web', 'code-review', + 'environment-build', 'discord', 'github', 'linear', diff --git a/services/cloud-agent-next/src/control-plane/sandbox/drizzle/0003_route_environment.sql b/services/cloud-agent-next/src/control-plane/sandbox/drizzle/0003_route_environment.sql new file mode 100644 index 00000000000..74a1821dd17 --- /dev/null +++ b/services/cloud-agent-next/src/control-plane/sandbox/drizzle/0003_route_environment.sql @@ -0,0 +1 @@ +ALTER TABLE `routes` ADD `environment` text; \ No newline at end of file diff --git a/services/cloud-agent-next/src/control-plane/sandbox/drizzle/meta/0003_snapshot.json b/services/cloud-agent-next/src/control-plane/sandbox/drizzle/meta/0003_snapshot.json new file mode 100644 index 00000000000..8c9dee17cdf --- /dev/null +++ b/services/cloud-agent-next/src/control-plane/sandbox/drizzle/meta/0003_snapshot.json @@ -0,0 +1,244 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "c0eeded8-563b-479c-bda8-a74be0bf0394", + "prevId": "bb1499e2-ca46-4ef5-8df4-7776e89d6ffa", + "tables": { + "allocation": { + "name": "allocation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allocation_id": { + "name": "allocation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "wrapper_id": { + "name": "wrapper_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_frame_at": { + "name": "last_frame_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "create_deadline_at": { + "name": "create_deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_connect_deadline_at": { + "name": "first_connect_deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stop_attempt": { + "name": "stop_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stop_pending": { + "name": "stop_pending", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stop_at": { + "name": "stop_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unconfirmed_provider_ref": { + "name": "unconfirmed_provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_pin": { + "name": "provider_pin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "routes": { + "name": "routes", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "spec": { + "name": "spec", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "grant": { + "name": "grant", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_source": { + "name": "credential_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repo_key": { + "name": "repo_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempt_id": { + "name": "attempt_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempt_deadline_at": { + "name": "attempt_deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scope_grants": { + "name": "scope_grants", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "grant": { + "name": "grant", + "type": "text", + "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/src/control-plane/sandbox/drizzle/meta/_journal.json b/services/cloud-agent-next/src/control-plane/sandbox/drizzle/meta/_journal.json index 9a7d1b66302..6f452a054d4 100644 --- a/services/cloud-agent-next/src/control-plane/sandbox/drizzle/meta/_journal.json +++ b/services/cloud-agent-next/src/control-plane/sandbox/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1790860790832, "tag": "0002_naive_deadpool", "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1792104724546, + "tag": "0003_route_environment", + "breakpoints": true } ] } \ No newline at end of file diff --git a/services/cloud-agent-next/src/control-plane/sandbox/drizzle/migrations.js b/services/cloud-agent-next/src/control-plane/sandbox/drizzle/migrations.js index 60e0d6430cc..92fde1909fd 100644 --- a/services/cloud-agent-next/src/control-plane/sandbox/drizzle/migrations.js +++ b/services/cloud-agent-next/src/control-plane/sandbox/drizzle/migrations.js @@ -2,6 +2,7 @@ import journal from './meta/_journal.json'; import m0000 from './0000_sandbox_control_v2.sql'; import m0001 from './0001_scope_grants.sql'; import m0002 from './0002_naive_deadpool.sql'; +import m0003 from './0003_route_environment.sql'; export default { journal, @@ -9,5 +10,6 @@ export default { m0000, m0001, m0002, + m0003, }, }; diff --git a/services/cloud-agent-next/src/control-plane/sandbox/environment-snapshot-launch.test.ts b/services/cloud-agent-next/src/control-plane/sandbox/environment-snapshot-launch.test.ts new file mode 100644 index 00000000000..579e9e2325c --- /dev/null +++ b/services/cloud-agent-next/src/control-plane/sandbox/environment-snapshot-launch.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, it } from 'vitest'; +import type { SnapshotResolution } from '../../environment-build/coordinator.js'; +import type { SessionEnvironmentReference } from '../../environment-build/reference.js'; +import { + environmentLaunchCandidate, + environmentStartDetail, + lookupEnvironmentSnapshot, + previousSnapshotStartFailed, + snapshotStart, +} from './environment-snapshot-launch.js'; +import type { RouteRecord } from './routes.js'; + +const REFERENCE: SessionEnvironmentReference = { + environmentId: '11111111-1111-4111-8111-111111111111', + owner: { type: 'user', id: 'user-1' }, + configurationRevision: 2, + repository: { platform: 'github', repoFullName: 'acme/widgets' }, +}; + +function route(overrides: Partial = {}): RouteRecord { + const sessionId = overrides.sessionId ?? 'workspace_1'; + return { + sessionId, + spec: { sessionId, kiloSessionId: 'ses_1', directory: '/workspace/app', attemptId: 'a1' }, + grant: null, + credentialSource: { + userId: 'user-1', + kiloSessionId: 'ses_1', + kiloToken: 'token', + }, + environment: REFERENCE, + state: 'preparing', + attemptId: 'a1', + attemptDeadlineAt: 0, + reason: null, + ...overrides, + }; +} + +const RESOLVED: SnapshotResolution = { + snapshot: { + snapshotId: 'snap-1', + baseImage: 'registry.example/app:1', + revision: 1, + capturedAt: Date.parse('2026-10-24T21:32:19Z'), + buildId: '22222222-2222-4222-8222-222222222222', + stale: 'revision', + }, + reason: 'eligible', + admission: { status: 'admitted', buildId: '33333333-3333-4333-8333-333333333333' }, +}; + +describe('environmentLaunchCandidate', () => { + it('selects the one environment of the isolated preparing route', () => { + expect(environmentLaunchCandidate([route()])).toEqual({ + candidate: { reference: REFERENCE, actorUserId: 'user-1' }, + }); + }); + + it('starts from the image for shared paths, other environments or no environment', () => { + expect( + environmentLaunchCandidate([ + route({ spec: { ...route().spec, directory: '/workspace/user/worktree' } }), + ]) + ).toEqual({ reason: 'not_isolated' }); + expect(environmentLaunchCandidate([route({ environment: null })])).toEqual({ + reason: 'no_environment', + }); + expect( + environmentLaunchCandidate([ + route(), + route({ + sessionId: 'workspace_2', + environment: { ...REFERENCE, configurationRevision: 3 }, + }), + ]) + ).toEqual({ reason: 'multiple_environments' }); + expect(environmentLaunchCandidate([route({ state: 'ready' })])).toEqual({ + reason: 'no_preparing_routes', + }); + }); +}); + +describe('previousSnapshotStartFailed', () => { + it('starts fresh once after a snapshot start that never connected or failed preparation', () => { + expect(previousSnapshotStartFailed(undefined)).toBe(false); + expect( + previousSnapshotStartFailed({ + allocationId: 'a', + startSource: 'repository', + confirmed: false, + }) + ).toBe(true); + expect( + previousSnapshotStartFailed({ + allocationId: 'a', + startSource: 'repository', + confirmed: true, + preparationFailures: [{ sessionId: 's', attemptId: 'a1' }], + }) + ).toBe(true); + expect( + previousSnapshotStartFailed({ allocationId: 'a', startSource: 'image', confirmed: true }) + ).toBe(false); + expect( + previousSnapshotStartFailed({ allocationId: 'a', skipRepository: true, confirmed: false }) + ).toBe(true); + }); +}); + +describe('environment snapshot lookup', () => { + it('returns the coordinator resolution within the budget', async () => { + const namespace = { + getByName: () => ({ resolveSnapshot: async () => RESOLVED }), + } as unknown as DurableObjectNamespace; + const lookup = await lookupEnvironmentSnapshot({ + namespace: namespace as never, + candidate: { reference: REFERENCE, actorUserId: 'user-1' }, + launchDeadlineAt: Date.now() + 60_000, + }); + expect(lookup).toEqual({ kind: 'resolved', resolution: RESOLVED }); + expect(snapshotStart(RESOLVED)).toEqual({ + snapshotId: 'snap-1', + baseImage: 'registry.example/app:1', + }); + }); + + it('times out at the launch deadline instead of waiting for the coordinator', async () => { + const namespace = { + getByName: () => ({ resolveSnapshot: () => new Promise(() => undefined) }), + }; + const started = Date.now(); + const lookup = await lookupEnvironmentSnapshot({ + namespace: namespace as never, + candidate: { reference: REFERENCE, actorUserId: 'user-1' }, + launchDeadlineAt: Date.now() + 50, + }); + expect(lookup).toEqual({ kind: 'timeout' }); + expect(Date.now() - started).toBeLessThan(1_000); + }); + + it('retries one transient failure and then starts without a snapshot', async () => { + let calls = 0; + const namespace = { + getByName: () => ({ + resolveSnapshot: async () => { + calls += 1; + throw new Error('transient'); + }, + }), + }; + await expect( + lookupEnvironmentSnapshot({ + namespace: namespace as never, + candidate: { reference: REFERENCE, actorUserId: 'user-1' }, + launchDeadlineAt: Date.now() + 60_000, + }) + ).resolves.toEqual({ kind: 'unavailable' }); + expect(calls).toBe(2); + await expect( + lookupEnvironmentSnapshot({ + namespace: undefined, + candidate: { reference: REFERENCE, actorUserId: 'user-1' }, + launchDeadlineAt: Date.now() + 60_000, + }) + ).resolves.toEqual({ kind: 'unavailable' }); + }); + + it('uses the same snapshot name when configuration revisions match', () => { + expect( + environmentStartDetail( + { ...REFERENCE, configurationRevision: 1 }, + { + kind: 'resolved', + resolution: { ...RESOLVED, admission: { status: 'not_needed' } }, + } + ) + ).toBe('Using snapshot · 24 Oct 2026, 21:32:19 UTC'); + }); + + it('names the snapshot by capture time and explains changed settings', () => { + expect(environmentStartDetail(REFERENCE, { kind: 'resolved', resolution: RESOLVED })).toBe( + 'Using snapshot · 24 Oct 2026, 21:32:19 UTC; environment settings have changed; cached files may retain earlier values; a replacement build was started' + ); + expect( + environmentStartDetail(REFERENCE, { + kind: 'resolved', + resolution: { + snapshot: null, + reason: 'missing', + admission: { status: 'skipped', reason: 'backoff', retryAt: 5 }, + }, + }) + ).toBe( + 'Starting without an environment snapshot; the replacement build is waiting after earlier failures' + ); + expect(environmentStartDetail(REFERENCE, { kind: 'timeout' })).toBe( + 'Starting without an environment snapshot (lookup unavailable)' + ); + }); +}); diff --git a/services/cloud-agent-next/src/control-plane/sandbox/environment-snapshot-launch.ts b/services/cloud-agent-next/src/control-plane/sandbox/environment-snapshot-launch.ts new file mode 100644 index 00000000000..adef4cab26f --- /dev/null +++ b/services/cloud-agent-next/src/control-plane/sandbox/environment-snapshot-launch.ts @@ -0,0 +1,172 @@ +import { environmentSnapshotName } from '../../shared/environment-snapshot-name.js'; +import { withTimeout } from '@kilocode/worker-utils'; +import type { EnvironmentBuild } from '../../environment-build/environment-build-do.js'; +import type { SnapshotAdmission, SnapshotResolution } from '../../environment-build/coordinator.js'; +import { SNAPSHOT_LOOKUP_BUDGET_MS } from '../../environment-build/policy.js'; +import type { SessionEnvironmentReference } from '../../environment-build/reference.js'; +import type { ProviderSnapshotStart } from '../../sandbox-control/provider.js'; +import { ISOLATED_CONTAINER_WORKSPACE_PATH } from '../../workspace.js'; +import type { RepositoryLaunchRecord } from './repository-launch.js'; +import type { RouteRecord } from './routes.js'; + +/** + * Environment snapshot selection for a new allocation (plan "Session launch"). + * Only isolated repository allocations consume environment snapshots: every + * route waiting for the allocation must sit at the isolated workspace path and + * carry the same admitted environment. The lookup is bounded and never waits + * for a build; a timeout, a miss or an unavailable coordinator is an ordinary + * start from the image. + */ + +export type EnvironmentLaunchCandidate = { + reference: SessionEnvironmentReference; + actorUserId: string; +}; + +export type EnvironmentCandidateResult = + | { candidate: EnvironmentLaunchCandidate } + | { + reason: + | 'no_preparing_routes' + | 'no_environment' + | 'not_isolated' + | 'multiple_environments' + | 'no_actor'; + }; + +export function environmentLaunchCandidate( + routes: readonly RouteRecord[] +): EnvironmentCandidateResult { + const preparing = routes.filter(route => route.state === 'preparing'); + if (preparing.length === 0) return { reason: 'no_preparing_routes' }; + if (preparing.some(route => route.spec.directory !== ISOLATED_CONTAINER_WORKSPACE_PATH)) { + return { reason: 'not_isolated' }; + } + const [first] = preparing; + const reference = first?.environment ?? null; + if (reference === null) return { reason: 'no_environment' }; + const same = preparing.every( + route => + route.environment !== null && + route.environment.environmentId === reference.environmentId && + route.environment.configurationRevision === reference.configurationRevision && + route.environment.owner.type === reference.owner.type && + route.environment.owner.id === reference.owner.id + ); + if (!same) return { reason: 'multiple_environments' }; + const actorUserId = first?.credentialSource?.userId; + if (actorUserId === undefined) return { reason: 'no_actor' }; + return { candidate: { reference, actorUserId } }; +} + +/** + * The previous snapshot start of this sandbox never connected or its preparation + * failed: this start uses the image once (one fresh fallback, plan "If snapshot + * restore fails, retry fresh once"). A completed image start clears it. + */ +export function previousSnapshotStartFailed(previous: RepositoryLaunchRecord | undefined): boolean { + return ( + previous?.skipRepository === true || + (previous?.startSource === 'repository' && !previous.confirmed) || + (previous?.startSource !== 'image' && (previous?.preparationFailures?.length ?? 0) > 0) + ); +} + +export type EnvironmentLookup = + | { kind: 'resolved'; resolution: SnapshotResolution } + | { kind: 'timeout' } + | { kind: 'unavailable' }; + +/** + * One bounded lookup: at most `SNAPSHOT_LOOKUP_BUDGET_MS`, capped by what is left + * of the launch deadline. A timeout does not cancel coordinator work; admission + * is idempotent, so a later lookup returns the same build. + */ +export async function lookupEnvironmentSnapshot(input: { + namespace: DurableObjectNamespace | undefined; + candidate: EnvironmentLaunchCandidate; + launchDeadlineAt: number; + now?: () => number; +}): Promise { + const now = input.now ?? Date.now; + if (input.namespace === undefined) return { kind: 'unavailable' }; + const deadlineAt = Math.min(now() + SNAPSHOT_LOOKUP_BUDGET_MS, input.launchDeadlineAt); + const stub = input.namespace.getByName(input.candidate.reference.environmentId); + for (let attempt = 1; attempt <= 2; attempt += 1) { + const remaining = deadlineAt - now(); + if (remaining <= 0) return { kind: 'timeout' }; + let timedOut = false; + try { + const resolution = (await withTimeout( + stub.resolveSnapshot({ + reference: input.candidate.reference, + actorUserId: input.candidate.actorUserId, + }), + remaining, + 'Environment snapshot lookup timed out', + () => { + timedOut = true; + } + )) as SnapshotResolution; + return { kind: 'resolved', resolution }; + } catch { + if (timedOut) return { kind: 'timeout' }; + } + } + return { kind: 'unavailable' }; +} + +export function snapshotStart(resolution: SnapshotResolution): ProviderSnapshotStart | undefined { + return resolution.snapshot === null + ? undefined + : { snapshotId: resolution.snapshot.snapshotId, baseImage: resolution.snapshot.baseImage }; +} + +function admissionText(admission: SnapshotAdmission): string | null { + switch (admission.status) { + case 'admitted': + return 'a replacement build was started'; + case 'active': + return 'a replacement build is running'; + case 'not_needed': + return null; + case 'skipped': + switch (admission.reason) { + case 'backoff': + return 'the replacement build is waiting after earlier failures'; + case 'container_limit_reached': + return 'no replacement build started: the container limit is reached'; + case 'build_in_progress': + case 'cleanup_pending': + return 'another environment build is still running'; + case 'revision_superseded': + return 'the environment changed after this session started'; + default: + return 'no replacement build started'; + } + } +} + +/** + * The preparation detail shown while the sandbox starts. It names the actual + * snapshot by capture time and the real replacement build status. Changed + * settings remain visible without exposing internal configuration revisions. + */ +export function environmentStartDetail( + reference: SessionEnvironmentReference, + lookup: EnvironmentLookup +): string { + if (lookup.kind !== 'resolved') { + return 'Starting without an environment snapshot (lookup unavailable)'; + } + const { snapshot, admission } = lookup.resolution; + const build = admissionText(admission); + if (snapshot === null) { + return `Starting without an environment snapshot${build === null ? '' : `; ${build}`}`; + } + const settings = + snapshot.revision === reference.configurationRevision + ? '' + : '; environment settings have changed; cached files may retain earlier values'; + return `Using snapshot · ${environmentSnapshotName(snapshot.capturedAt)}${settings}${build === null ? '' : `; ${build}`}`; +} diff --git a/services/cloud-agent-next/src/control-plane/sandbox/repo-key.test.ts b/services/cloud-agent-next/src/control-plane/sandbox/repo-key.test.ts deleted file mode 100644 index 7989bde02b3..00000000000 --- a/services/cloud-agent-next/src/control-plane/sandbox/repo-key.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - computeRepoKey, - repoSnapshotEligible, - repoSnapshotEligibility, - type RepoSnapshotGate, -} from './repo-key.js'; -import { ISOLATED_CONTAINER_WORKSPACE_PATH } from '../../workspace.js'; - -const USER = 'user_123'; -const ORG = 'org_123'; -const REPO_URL = 'https://github.com/acme/widgets.git'; - -function gate(overrides: Partial = {}): RepoSnapshotGate { - return { - enrolledIds: undefined, - userId: USER, - orgId: ORG, - ...overrides, - }; -} - -const route = { repoUrl: REPO_URL, directory: ISOLATED_CONTAINER_WORKSPACE_PATH }; - -describe('repoSnapshotEligible', () => { - it('enrolls a personal owner by user ID from the unified flag', () => { - expect(repoSnapshotEligible(gate({ enrolledIds: USER, orgId: undefined }), route)).toBe(true); - }); - - it('enrolls an org owner by org ID from the unified flag', () => { - expect(repoSnapshotEligible(gate({ enrolledIds: ORG }), route)).toBe(true); - }); - - it("enrolls everyone for the unified flag's wildcard", () => { - expect(repoSnapshotEligible(gate({ enrolledIds: '*' }), route)).toBe(true); - expect(repoSnapshotEligible(gate({ enrolledIds: '*', orgId: undefined }), route)).toBe(true); - }); - - it('does not enroll a non-matching user or org', () => { - expect(repoSnapshotEligible(gate({ enrolledIds: 'user_other,org_other' }), route)).toBe(false); - expect( - repoSnapshotEligible(gate({ enrolledIds: 'user_other,org_other', orgId: undefined }), route) - ).toBe(false); - }); - - it('requires a repository at the isolated constant path', () => { - expect(repoSnapshotEligible(gate({ enrolledIds: '*' }), { ...route, repoUrl: undefined })).toBe( - false - ); - expect( - repoSnapshotEligible(gate({ enrolledIds: '*' }), { - ...route, - directory: '/workspace/org/user/sessions/s1', - }) - ).toBe(false); - }); -}); - -describe('repoSnapshotEligibility', () => { - it('names the gate that rejected the route', () => { - expect(repoSnapshotEligibility(gate({ enrolledIds: 'user_other,org_other' }), route)).toEqual({ - eligible: false, - reason: 'not_enrolled', - }); - expect( - repoSnapshotEligibility(gate({ enrolledIds: '*' }), { ...route, repoUrl: undefined }) - ).toEqual({ eligible: false, reason: 'no_repo_url' }); - expect( - repoSnapshotEligibility(gate({ enrolledIds: '*' }), { - ...route, - directory: '/workspace/org/user/sessions/s1', - }) - ).toEqual({ eligible: false, reason: 'directory_not_isolated' }); - }); - - it('reports an enrolled route with a repository at the isolated path as eligible', () => { - expect(repoSnapshotEligibility(gate({ enrolledIds: '*' }), route)).toEqual({ eligible: true }); - }); -}); - -describe('computeRepoKey', () => { - it('returns no key without a secret or a repository', async () => { - expect(await computeRepoKey({ secret: null, userId: USER, ...route })).toBeNull(); - expect( - await computeRepoKey({ - secret: 'secret', - userId: USER, - repoUrl: undefined, - directory: route.directory, - }) - ).toBeNull(); - }); - - it('scopes the key to the user and the repository', async () => { - const base = await computeRepoKey({ secret: 'secret', userId: USER, ...route }); - const otherUser = await computeRepoKey({ secret: 'secret', userId: 'user_other', ...route }); - const otherRepo = await computeRepoKey({ - secret: 'secret', - userId: USER, - ...route, - repoUrl: 'https://github.com/acme/other.git', - }); - expect(base).toMatch(/^[0-9a-f]{64}$/); - expect(otherUser).not.toBe(base); - expect(otherRepo).not.toBe(base); - }); - - it('folds the setup commands into the key and keeps the no-setup key unchanged', async () => { - const noSetup = await computeRepoKey({ secret: 'secret', userId: USER, ...route }); - const empty = await computeRepoKey({ - secret: 'secret', - userId: USER, - ...route, - setupCommands: [], - }); - const install = await computeRepoKey({ - secret: 'secret', - userId: USER, - ...route, - setupCommands: ['pnpm install'], - }); - const other = await computeRepoKey({ - secret: 'secret', - userId: USER, - ...route, - setupCommands: ['pnpm build'], - }); - const installAgain = await computeRepoKey({ - secret: 'secret', - userId: USER, - ...route, - setupCommands: ['pnpm install'], - }); - expect(empty).toBe(noSetup); - expect(install).not.toBe(noSetup); - expect(other).not.toBe(install); - expect(installAgain).toBe(install); - }); -}); diff --git a/services/cloud-agent-next/src/control-plane/sandbox/repo-key.ts b/services/cloud-agent-next/src/control-plane/sandbox/repo-key.ts deleted file mode 100644 index 6c977f82fdb..00000000000 --- a/services/cloud-agent-next/src/control-plane/sandbox/repo-key.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { isOrgInList } from '../../sandbox-id.js'; -import { ISOLATED_CONTAINER_WORKSPACE_PATH } from '../../workspace.js'; - -const REPO_KEY_DOMAIN = 'kilo:repo-snapshot-key:v1'; - -export type RepoKeyInput = { - /** The Worker secret the hash is keyed with; absent disables repository snapshots. */ - secret: string | null; - /** Snapshot scope. Per user for now; widening to the org is a change to this field. */ - userId: string; - repoUrl: string | undefined; - directory: string; - /** Setup commands the snapshot was captured after; part of the snapshot identity. */ - setupCommands?: string[]; -}; - -export type RepoSnapshotGate = { - /** User or org IDs admitted. `*` includes everyone. */ - enrolledIds: string | undefined; - userId: string; - orgId: string | undefined; -}; - -export type RepoSnapshotIneligibilityReason = - | 'not_enrolled' - | 'no_repo_url' - | 'directory_not_isolated'; - -export type RepoSnapshotEligibility = - | { eligible: true } - | { eligible: false; reason: RepoSnapshotIneligibilityReason }; - -/** - * Why a route is or is not eligible to use a repository snapshot. This is the one - * owner of the decision; `repoSnapshotEligible` is the boolean projection for - * callers that do not need the reason (the reason is logged when the key is null). - * - * The owner (personal user, or the org when the session is org-owned) must be - * enrolled, and the route must have a repository at the constant path the - * snapshots are taken at. A snapshot holds one repository at one path, so a - * per-session path or a shared sandbox could only capture something no later - * session can reuse. - * - * Enrollment follows the `SANDBOX_SELECTION_IDS` allowlist convention: - * one list matched against the user ID and then the org ID. - */ -export function repoSnapshotEligibility( - gate: RepoSnapshotGate, - route: Pick -): RepoSnapshotEligibility { - if (!(isOrgInList(gate.enrolledIds, gate.userId) || isOrgInList(gate.enrolledIds, gate.orgId))) { - return { eligible: false, reason: 'not_enrolled' }; - } - if (route.repoUrl === undefined) return { eligible: false, reason: 'no_repo_url' }; - if (route.directory !== ISOLATED_CONTAINER_WORKSPACE_PATH) { - return { eligible: false, reason: 'directory_not_isolated' }; - } - return { eligible: true }; -} - -export function repoSnapshotEligible( - gate: RepoSnapshotGate, - route: Pick -): boolean { - return repoSnapshotEligibility(gate, route).eligible; -} - -/** - * The keyed hash that names a repository snapshot: scope, repository and the setup - * commands that produced it. Env is not part of it: setup re-runs on every start - * and rewrites what it derives from the env. Because capture runs after setup, the - * setup commands join the hash so a snapshot is reused only by a route whose setup - * matches; a different setup for the same repository gets its own key instead of - * adopting output it did not produce. The digest is hashed under a Worker secret so - * it cannot be guessed to address another user's snapshot. - */ -export async function computeRepoKey(input: RepoKeyInput): Promise { - if (input.secret === null || input.repoUrl === undefined) return null; - const setup = await setupCommandsDigest(input.setupCommands); - const message = JSON.stringify( - setup === null - ? [REPO_KEY_DOMAIN, input.userId, input.repoUrl] - : [REPO_KEY_DOMAIN, input.userId, input.repoUrl, setup] - ); - const encoder = new TextEncoder(); - const key = await crypto.subtle.importKey( - 'raw', - encoder.encode(input.secret), - { name: 'HMAC', hash: 'SHA-256' }, - false, - ['sign'] - ); - const digest = new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(message))); - return Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join(''); -} - -/** - * A digest of the setup commands, or null when there are none. Absent and empty - * are the same, so a route without setup keeps the key computed before setup - * joined the identity. The commands are hashed rather than embedded so a secret - * bearing command never enters the HMAC message. - */ -async function setupCommandsDigest(setupCommands: string[] | undefined): Promise { - if (setupCommands === undefined || setupCommands.length === 0) return null; - const bytes = new TextEncoder().encode(JSON.stringify(setupCommands)); - const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)); - return Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join(''); -} diff --git a/services/cloud-agent-next/src/control-plane/sandbox/repository-launch.test.ts b/services/cloud-agent-next/src/control-plane/sandbox/repository-launch.test.ts deleted file mode 100644 index cccb4ac8a6b..00000000000 --- a/services/cloud-agent-next/src/control-plane/sandbox/repository-launch.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { repositoryLaunchOptions, type RepositoryLaunchRecord } from './repository-launch.js'; -import type { RouteRecord } from './routes.js'; - -function preparingRoute( - overrides: Partial> = {} -): RouteRecord { - const sessionId = overrides.sessionId ?? 'workspace_1'; - return { - sessionId, - spec: { - sessionId, - kiloSessionId: 'ses_1', - directory: '/workspace/app', - attemptId: 'attempt_1', - }, - grant: null, - credentialSource: null, - repoKey: 'repo-key', - state: 'preparing', - attemptId: 'attempt_1', - attemptDeadlineAt: 0, - reason: null, - ...overrides, - }; -} - -describe('repositoryLaunchOptions', () => { - it('reports no_preparing_routes when no route is waiting', () => { - const decision = repositoryLaunchOptions([preparingRoute({ state: 'ready' })], undefined); - expect(decision).toEqual({ - options: {}, - reason: 'no_preparing_routes', - preparingRouteCount: 0, - distinctKeyCount: 0, - skipped: false, - }); - }); - - it('reports no_repo_key when the waiting route has no key', () => { - const decision = repositoryLaunchOptions([preparingRoute({ repoKey: null })], undefined); - expect(decision.reason).toBe('no_repo_key'); - expect(decision.distinctKeyCount).toBe(1); - expect(decision.options).toEqual({}); - }); - - it('reports multiple_keys when waiting routes disagree', () => { - const decision = repositoryLaunchOptions( - [ - preparingRoute({ repoKey: 'a' }), - preparingRoute({ sessionId: 'workspace_2', repoKey: 'b' }), - ], - undefined - ); - expect(decision.reason).toBe('multiple_keys'); - expect(decision.distinctKeyCount).toBe(2); - expect(decision.options).toEqual({}); - }); - - it('chooses the one shared key', () => { - const decision = repositoryLaunchOptions( - [ - preparingRoute({ repoKey: 'shared' }), - preparingRoute({ sessionId: 'workspace_2', repoKey: 'shared' }), - ], - undefined - ); - expect(decision.reason).toBe('repository'); - expect(decision.options).toEqual({ repoKey: 'shared' }); - expect(decision.skipped).toBe(false); - }); - - it('skips a previous repository start whose wrapper never connected', () => { - const previous: RepositoryLaunchRecord = { - allocationId: 'allocation_1', - startSource: 'repository', - confirmed: false, - }; - const decision = repositoryLaunchOptions([preparingRoute()], previous); - expect(decision.skipped).toBe(true); - expect(decision.options).toEqual({ repoKey: 'repo-key', skipRepository: true }); - }); - - it('keeps a confirmed repository start', () => { - const previous: RepositoryLaunchRecord = { - allocationId: 'allocation_1', - startSource: 'repository', - confirmed: true, - }; - const decision = repositoryLaunchOptions([preparingRoute()], previous); - expect(decision.skipped).toBe(false); - expect(decision.options).toEqual({ repoKey: 'repo-key' }); - }); - - it('skips a confirmed repository start whose preparation failed', () => { - const decision = repositoryLaunchOptions([preparingRoute()], { - allocationId: 'allocation_1', - startSource: 'repository', - confirmed: true, - preparationFailures: [{ sessionId: 'workspace_1', attemptId: 'attempt_1' }], - }); - expect(decision.skipped).toBe(true); - expect(decision.options).toEqual({ repoKey: 'repo-key', skipRepository: true }); - }); - - it('retains the repository skip while an image launch has not completed', () => { - const decision = repositoryLaunchOptions([preparingRoute()], { - allocationId: 'allocation_2', - skipRepository: true, - confirmed: false, - }); - expect(decision.skipped).toBe(true); - expect(decision.options).toEqual({ repoKey: 'repo-key', skipRepository: true }); - }); - - it('does not skip an image start whose preparation failed', () => { - const decision = repositoryLaunchOptions([preparingRoute()], { - allocationId: 'allocation_2', - startSource: 'image', - confirmed: true, - preparationFailures: [{ sessionId: 'workspace_1', attemptId: 'attempt_1' }], - }); - expect(decision.skipped).toBe(false); - }); - - it('skips an unresolved launch whose preparation failed', () => { - const decision = repositoryLaunchOptions([preparingRoute()], { - allocationId: 'allocation_1', - confirmed: true, - preparationFailures: [{ sessionId: 'workspace_1', attemptId: 'attempt_1' }], - }); - expect(decision.skipped).toBe(true); - }); -}); diff --git a/services/cloud-agent-next/src/control-plane/sandbox/repository-launch.ts b/services/cloud-agent-next/src/control-plane/sandbox/repository-launch.ts index 2ab8611311e..de0eb992edc 100644 --- a/services/cloud-agent-next/src/control-plane/sandbox/repository-launch.ts +++ b/services/cloud-agent-next/src/control-plane/sandbox/repository-launch.ts @@ -1,10 +1,5 @@ import type { StartupMode } from '../../sandbox-control/startup-selection.js'; -import type { - ProviderAdapter, - ProviderLaunchOptions, - ProviderStartSource, -} from '../../sandbox-control/provider.js'; -import type { RouteRecord } from './routes.js'; +import type { ProviderStartSource } from '../../sandbox-control/provider.js'; import type { ControlPlaneWorkspaceFailureSubtype } from '../../shared/control-plane-protocol.js'; const LAUNCH_KEY = 'repository_launch'; @@ -63,87 +58,3 @@ export async function confirmRepositoryLaunch( if (record === undefined || record.allocationId !== allocationId || record.confirmed) return; await recordRepositoryLaunch(storage, { ...record, confirmed: true }); } - -/** Why a launch did or did not ask the provider to start from a repository snapshot. */ -export type RepositoryLaunchReason = - | 'no_preparing_routes' - | 'no_repo_key' - | 'multiple_keys' - | 'repository'; - -/** - * A launch's snapshot decision plus what drove it, so one log line can explain a - * start from the image without re-deriving the rule at the call site. - */ -export type RepositoryLaunchDecision = { - options: ProviderLaunchOptions; - reason: RepositoryLaunchReason; - preparingRouteCount: number; - /** Distinct keys across preparing routes; `> 1` is why a snapshot was not used. */ - distinctKeyCount: number; - /** The previous repository launch or preparation failed. */ - skipped: boolean; -}; - -/** - * The launch options for a new allocation. The key comes from the routes waiting - * for it: exactly one key shared by all of them, else no snapshot. When the - * previous repository allocation never connected or failed preparation, this - * start uses the image without deleting the snapshot. - */ -export function repositoryLaunchOptions( - routes: readonly RouteRecord[], - previous: RepositoryLaunchRecord | undefined -): RepositoryLaunchDecision { - const preparing = routes.filter(route => route.state === 'preparing'); - const keys = new Set(preparing.map(route => route.repoKey)); - const preparingRouteCount = preparing.length; - const distinctKeyCount = keys.size; - if (preparingRouteCount === 0) { - return { - options: {}, - reason: 'no_preparing_routes', - preparingRouteCount, - distinctKeyCount, - skipped: false, - }; - } - if (distinctKeyCount !== 1) { - return { - options: {}, - reason: 'multiple_keys', - preparingRouteCount, - distinctKeyCount, - skipped: false, - }; - } - const [repoKey] = keys; - if (repoKey === null || repoKey === undefined) { - return { - options: {}, - reason: 'no_repo_key', - preparingRouteCount, - distinctKeyCount, - skipped: false, - }; - } - const previousStartFailed = - previous?.skipRepository === true || - (previous?.startSource === 'repository' && !previous.confirmed) || - (previous?.startSource !== 'image' && (previous?.preparationFailures?.length ?? 0) > 0); - return { - options: { repoKey, ...(previousStartFailed ? { skipRepository: true as const } : {}) }, - reason: 'repository', - preparingRouteCount, - distinctKeyCount, - skipped: previousStartFailed, - }; -} - -/** Whether `session.prepare` should ask the wrapper to save the prepared workspace. */ -export function captureRequested( - route: Pick, - provider: Pick -): boolean { - return route.repoKey !== null && provider.captureRepository !== undefined; -} diff --git a/services/cloud-agent-next/src/control-plane/sandbox/routes.ts b/services/cloud-agent-next/src/control-plane/sandbox/routes.ts index b21988b7a04..811b274fd60 100644 --- a/services/cloud-agent-next/src/control-plane/sandbox/routes.ts +++ b/services/cloud-agent-next/src/control-plane/sandbox/routes.ts @@ -1,3 +1,7 @@ +import { + environmentReferenceSchema, + type SessionEnvironmentReference, +} from '../../environment-build/reference.js'; import { eq } from 'drizzle-orm'; import type { drizzle } from 'drizzle-orm/durable-sqlite'; import { @@ -49,11 +53,10 @@ export type RouteRecord = { grant: RouteGrant | null; credentialSource: ControlPlaneCredentialSource | null; /** - * Names the repository snapshot this route may start from and capture. Computed - * from the input spec when the route is first prepared and kept across - * attempts, because the stored spec no longer has the input env. + * The repository environment the session admitted, for environment snapshot + * selection. DO-private like the credential source; kept across attempts. */ - repoKey: string | null; + environment: SessionEnvironmentReference | null; state: RouteState; attemptId: string; attemptDeadlineAt: number; @@ -123,7 +126,10 @@ export function routeFromRow(db: RouteDatabase, row: RouteRow): RouteRecord { row.credential_source === null || row.credential_source === undefined ? null : controlPlaneCredentialSourceSchema.parse(JSON.parse(row.credential_source)), - repoKey: row.repo_key ?? null, + environment: + row.environment === null || row.environment === undefined + ? null + : environmentReferenceSchema.parse(JSON.parse(row.environment)), state: parseRouteState(row.state), attemptId: row.attempt_id, attemptDeadlineAt: row.attempt_deadline_at ?? 0, @@ -138,7 +144,7 @@ function routeToRow(route: RouteRecord): typeof routesTable.$inferInsert { grant: route.grant === null ? null : scopeGrantId(route.grant), credential_source: route.credentialSource === null ? null : JSON.stringify(route.credentialSource), - repo_key: route.repoKey, + environment: route.environment === null ? null : JSON.stringify(route.environment), state: route.state, attempt_id: route.attemptId, attempt_deadline_at: route.attemptDeadlineAt, @@ -153,14 +159,14 @@ export function newPreparingRoute( attemptDeadlineAt: number, grant: RouteGrant | null, credentialSource: ControlPlaneCredentialSource | null, - repoKey: string | null + environment: SessionEnvironmentReference | null ): RouteRecord { return { sessionId: spec.sessionId, spec, grant, credentialSource, - repoKey, + environment, state: 'preparing', attemptId, attemptDeadlineAt, @@ -284,7 +290,7 @@ export async function failRoute( spec: redactRouteSpec(spec), grant: existing?.grant ?? null, credentialSource: existing?.credentialSource ?? null, - repoKey: null, + environment: existing?.environment ?? null, state: 'failed', attemptId, attemptDeadlineAt: 0, @@ -352,7 +358,7 @@ export async function startAttempt( ctx: RouteContext, spec: ControlPlaneRouteSpec, source: ControlPlaneCredentialSource | null, - repoKey: string | null + environment: SessionEnvironmentReference | null ): Promise { const attemptId = crypto.randomUUID(); const attemptSpec: ControlPlaneRouteSpec = { ...spec, attemptId }; @@ -393,7 +399,7 @@ export async function startAttempt( ctx.now() + ctx.routePreparationMs, issued.grant, source, - repoKey + environment ); for (let attempt = 1; attempt <= ATTEMPT_POLICY_ATTEMPTS; attempt += 1) { if (await ctx.applyPolicy(issued.grant)) { @@ -420,13 +426,13 @@ export async function ensureRoute( ctx: RouteContext, spec: ControlPlaneRouteSpec, source: ControlPlaneCredentialSource | null, - repoKey: string | null + environment: SessionEnvironmentReference | null ): Promise<{ route: RouteRecord; started: boolean }> { const existing = await readRoute(ctx.db, spec.sessionId); if (existing !== null && existing.state !== 'failed') { return { route: existing, started: false }; } - return { route: await startAttempt(ctx, spec, source, repoKey), started: true }; + return { route: await startAttempt(ctx, spec, source, environment), started: true }; } /** Ready routes after `hello`: a restart starts a new attempt, else re-notify. */ @@ -439,7 +445,7 @@ export async function onWrapperConnected(ctx: RouteContext, restarted: boolean): attemptId: route.attemptId, reason: 'agent_restarted', }); - await startAttempt(ctx, route.spec, route.credentialSource, route.repoKey); + await startAttempt(ctx, route.spec, route.credentialSource, route.environment); continue; } if (route.state === 'ready') { 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..f7524ac12e8 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 @@ -65,7 +65,7 @@ import { } from '../../sandbox-control/vercel-provider.js'; import { buildControlWrapperLaunchEnv } from '../../sandbox-control/wrapper-launch-env.js'; import { downloadedStartup } from '../../sandbox-control/downloaded-startup.js'; -import { selectStartup, startupRepositoryKey } from '../../sandbox-control/startup-selection.js'; +import { selectStartup } from '../../sandbox-control/startup-selection.js'; import { STARTUP_FAILURES, type StartupFailureCode, @@ -263,17 +263,26 @@ import { scopeGrantId, writeScopeGrant, } from './scope-grants.js'; -import { computeRepoKey, repoSnapshotEligibility } from './repo-key.js'; import { buildFrameEnv } from './frame-env.js'; import { beginRepositoryLaunch, - captureRequested, confirmRepositoryLaunch, readRepositoryLaunch, recordRepositoryLaunch, - repositoryLaunchOptions, type RepositoryPreparationFailure, } from './repository-launch.js'; +import { + environmentLaunchCandidate, + environmentStartDetail, + lookupEnvironmentSnapshot, + previousSnapshotStartFailed, + snapshotStart, +} from './environment-snapshot-launch.js'; +import { + environmentReferenceSchema, + type SessionEnvironmentReference, +} from '../../environment-build/reference.js'; +import type { ProviderLaunchOptions } from '../../sandbox-control/provider.js'; import { beginWorktreeDeletion, controlPlaneWorktreeDeletionInputSchema, @@ -311,8 +320,7 @@ const GENERATION_KEY = 'control_plane_generation'; const GENERATION = 2; const CREDENTIAL_HASH_KEY = 'wrapper_credential_hash'; const OWNER_KEY = 'control_plane_owner'; -/** Backstop over the container DO's own capture timeout; the wrapper waits slightly longer. */ -const REPOSITORY_CAPTURE_CALL_MS = 5 * 60_000 + 5_000; + const ALLOCATION_ROW_ID = 'current'; export type StoredProviderPin = { @@ -625,10 +633,6 @@ export class SandboxControlV2 extends DurableObject { private provider: ProviderAdapter; private providerPin: StoredProviderPin | null = null; private createInFlight = false; - private readonly repositoryCaptures = new Map< - string, - { allocationId: string | null; ok?: boolean } - >(); private readonly worktreeChanges = createSharedWorktreeChanges({ storage: this.ctx.storage, capture: input => this.worktreeCapture(input), @@ -806,7 +810,13 @@ export class SandboxControlV2 extends DurableObject { spec: input.spec, credentials: input.credentials, }); - return this.enqueue(() => this.runPrepare(parsed.spec, parsed.credentials, sandboxSelection)); + const environment = + input.environment === undefined + ? undefined + : environmentReferenceSchema.parse(input.environment); + return this.enqueue(() => + this.runPrepare(parsed.spec, parsed.credentials, sandboxSelection, environment) + ); } async deliver(payload: ControlPlaneDeliverPayload): Promise { @@ -840,7 +850,6 @@ export class SandboxControlV2 extends DurableObject { const parsed = controlPlaneSessionRefPayloadSchema.parse(payload); await this.enqueue(async () => { this.notifications.retire(parsed.sessionId); - this.repositoryCaptures.delete(parsed.sessionId); const released = await readRoute(this.db, parsed.sessionId); // A shared canonical handle survives on the other members, so removing // this route's path rules is not worth a policy rebuild: a transient @@ -1816,13 +1825,6 @@ export class SandboxControlV2 extends DurableObject { ); await this.enqueue(async () => { const attachment = this.readAttachment(ws); - if ( - attachment?.allocationId !== undefined && - [...this.repositoryCaptures.values()].some( - capture => capture.allocationId === attachment.allocationId && capture.ok === undefined - ) - ) - return; if (attachment?.allocationId === undefined || attachment.connectionId === undefined) { ws.serializeAttachment({ credential: null }); await this.armAlarm(await this.readAllocation()); @@ -2132,11 +2134,12 @@ export class SandboxControlV2 extends DurableObject { this.currentProvider() === 'cloudflare-containers' && (event.type === 'ensure' || event.type === 'tick') ) { - const launch = repositoryLaunchOptions( - await listRoutes(this.db), - await readRepositoryLaunch(this.ctx.storage) - ); - if (launch.options.repoKey !== undefined && !launch.skipped) { + // A start that may restore an environment snapshot gets the snapshot + // startup budget; selection itself happens in the create. + if ( + 'candidate' in environmentLaunchCandidate(await listRoutes(this.db)) && + !previousSnapshotStartFailed(await readRepositoryLaunch(this.ctx.storage)) + ) { timers.providerCreateMs = DEADLINE_MS.repositoryStartup; } } @@ -2163,16 +2166,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') { - this.repositoryCaptures.delete(sessionId); - } else if ( - capture.ok !== undefined && - (await readRoute(this.db, sessionId))?.state !== 'preparing' - ) { - this.repositoryCaptures.delete(sessionId); - } - } for (const effect of effects) { await this.runEffect(effect, state); } @@ -2225,7 +2218,8 @@ export class SandboxControlV2 extends DurableObject { private async runPrepare( spec: ControlPlaneRouteSpec, source: ControlPlaneCredentialSource, - sandboxSelection?: ControlPlaneSandboxSelection + sandboxSelection?: ControlPlaneSandboxSelection, + environment?: SessionEnvironmentReference ): Promise { // Sandbox owner admission (B3 review 5): the first prepare names the owner; // a later prepare for a different owner is rejected, not silently accepted. @@ -2353,8 +2347,12 @@ export class SandboxControlV2 extends DurableObject { } } } - const repoKey = await this.repoKeyFor(spec, source); - const { route, started } = await ensureRoute(this.routeContext(state), spec, source, repoKey); + const { route, started } = await ensureRoute( + this.routeContext(state), + spec, + source, + environment ?? null + ); if (route.state !== 'failed') { if (started) await this.armAlarm(state); if (state.kind === 'stopped') { @@ -2416,69 +2414,6 @@ export class SandboxControlV2 extends DurableObject { await this.ctx.storage.put(OWNER_KEY, normalized); } - /** - * The repository snapshot key for a route, or null when none applies. It is hashed - * from the input spec: the grant rewrites `spec.env` with per-attempt credential - * aliases, so the stored spec would give every session its own key. The setup - * commands are folded into the key, so a snapshot is reused only by a route with - * the same setup; a different setup neither restores nor captures under it. - * - * Emits one `snapshot_key` diagnostic per route so a missing snapshot can be - * traced to the gate that rejected it (or to a missing key secret). - */ - private async repoKeyFor( - spec: ControlPlaneRouteSpec, - source: ControlPlaneCredentialSource - ): Promise { - if (this.provider.captureRepository === undefined) { - this.logSnapshotKey(spec.sessionId, false, 'provider_no_capture'); - return null; - } - const route = { repoUrl: spec.git?.url, directory: spec.directory }; - const gate = { - enrolledIds: this.env.CONTAINER_REPO_SNAPSHOT_IDS, - userId: source.userId, - orgId: source.orgId, - }; - const eligibility = repoSnapshotEligibility(gate, route); - if (!eligibility.eligible) { - this.logSnapshotKey(spec.sessionId, false, eligibility.reason); - return null; - } - const secret = await withTimeout( - resolveSecret(this.env.NEXTAUTH_SECRET), - 1_000, - 'Repository snapshot key secret lookup timed out' - ).catch(() => null); - const key = await computeRepoKey({ - secret, - userId: source.userId, - ...route, - setupCommands: spec.setupCommands, - }); - if (key === null) { - this.logSnapshotKey(spec.sessionId, false, 'secret_missing'); - return null; - } - this.logSnapshotKey(spec.sessionId, true, 'eligible', key); - return key; - } - - private logSnapshotKey( - sessionId: string, - eligible: boolean, - reason: string, - repoKey?: string - ): void { - logControlDiagnostic('snapshot_key', { - sandboxId: this.sandboxId, - sessionId, - eligible, - reason, - repoKey: repoKey?.slice(0, 16), - }); - } - private async requireOwner(): Promise { const stored = await this.ctx.storage.get(OWNER_KEY); return typeof stored === 'string' && stored.length > 0 ? stored : null; @@ -3547,7 +3482,6 @@ export class SandboxControlV2 extends DurableObject { ...(frameEnv.env === undefined ? {} : { env: frameEnv.env }), ...(mcp === undefined ? {} : { mcp }), ...(frameEnv.secretEnvKeys.length === 0 ? {} : { secretEnvKeys: frameEnv.secretEnvKeys }), - ...(captureRequested(route, this.provider) ? { capture: true as const } : {}), }); } catch (error) { if (error instanceof PrepareFrameMaterializationError) throw error; @@ -4315,37 +4249,14 @@ export class SandboxControlV2 extends DurableObject { return; } const launchDeadline = current.createDeadlineAt ?? createDeadline; - const launch = repositoryLaunchOptions( - await listRoutes(this.db), - await readRepositoryLaunch(this.ctx.storage) - ); - logControlDiagnostic('snapshot_launch', { - sandboxId: this.sandboxId, - allocationId, - reason: launch.reason, - preparingRoutes: launch.preparingRouteCount, - distinctKeys: launch.distinctKeyCount, - skip: launch.skipped, - repoKey: launch.options.repoKey?.slice(0, 16), - }); - if (launch.skipped) { - await notifyPreparingRoutes(this.routeContext(current), { - step: 'snapshot_fallback', - detail: 'Previous snapshot startup failed. Retrying without a snapshot.', - }); - } + const launch = await this.environmentSnapshotLaunch(current, allocationId, launchDeadline); // Persist a placeholder before the launch so a `hello` accepted while it // runs confirms this allocation, and that confirmation outlives the // allocation stopping before the launch resolves. await beginRepositoryLaunch(this.ctx.storage, allocationId, launch.skipped, selection.mode); if (Date.now() >= launchDeadline) throw new Error('Sandbox launch timed out'); - const launchOptions = { + const launchOptions: ProviderLaunchOptions = { ...launch.options, - ...(launch.options.repoKey === undefined - ? {} - : { - repoKey: startupRepositoryKey(launch.options.repoKey, selection.mode), - }), ...(pin.provider === 'cloudflare-containers' ? { deadlineAt: launchDeadline } : {}), }; let launched = await withTimeout( @@ -4405,8 +4316,6 @@ export class SandboxControlV2 extends DurableObject { createdRef, { command: startupCommand, env: imageLaunchEnv }, { - ...launchOptions, - skipRepository: true, deadlineAt: launchDeadline, } ), @@ -4475,6 +4384,70 @@ export class SandboxControlV2 extends DurableObject { } } + /** + * Environment snapshot selection for one new allocation (plan "Session + * launch"). It runs off the serial queue inside the create, bounded at 15 + * seconds and by the launch deadline. It may admit a background build but + * never waits for one; any miss is an ordinary start from the image. The + * previous start of this sandbox failing from a snapshot makes this one + * fresh, once. + */ + private async environmentSnapshotLaunch( + state: AllocationState, + allocationId: string, + launchDeadlineAt: number + ): Promise<{ options: ProviderLaunchOptions; skipped: boolean }> { + const routes = await listRoutes(this.db); + const selected = environmentLaunchCandidate(routes); + const log = (fields: Record) => + logControlDiagnostic('snapshot_launch', { + sandboxId: this.sandboxId, + allocationId, + ...fields, + }); + if (this.provider.environmentSnapshots !== true) { + log({ reason: 'provider_no_snapshots' }); + return { options: {}, skipped: false }; + } + if (!('candidate' in selected)) { + log({ reason: selected.reason }); + return { options: {}, skipped: false }; + } + const { candidate } = selected; + if (previousSnapshotStartFailed(await readRepositoryLaunch(this.ctx.storage))) { + log({ reason: 'previous_snapshot_start_failed', skip: true }); + await notifyPreparingRoutes(this.routeContext(state), { + step: 'snapshot_fallback', + detail: 'Previous snapshot startup failed. Retrying without a snapshot.', + }); + return { options: {}, skipped: true }; + } + const lookup = await lookupEnvironmentSnapshot({ + namespace: this.env.ENVIRONMENT_BUILD, + candidate, + launchDeadlineAt, + }); + const snapshot = lookup.kind === 'resolved' ? snapshotStart(lookup.resolution) : undefined; + log({ + reason: lookup.kind === 'resolved' ? lookup.resolution.reason : lookup.kind, + admission: lookup.kind === 'resolved' ? lookup.resolution.admission.status : undefined, + sessionRevision: candidate.reference.configurationRevision, + snapshotRevision: + lookup.kind === 'resolved' + ? (lookup.resolution.snapshot?.revision ?? undefined) + : undefined, + stale: + lookup.kind === 'resolved' ? (lookup.resolution.snapshot?.stale ?? undefined) : undefined, + }); + // Its own step: the later `sandbox_start` progress would replace this + // detail, and the completed timeline keeps each step's last detail. + await notifyPreparingRoutes(this.routeContext(state), { + step: 'environment_snapshot', + detail: environmentStartDetail(candidate.reference, lookup), + }); + return { options: snapshot === undefined ? {} : { snapshot }, skipped: false }; + } + /** * Remember how this allocation's container started. A wrapper that connected * while a slow launch was still returning has already confirmed the start @@ -4546,7 +4519,7 @@ export class SandboxControlV2 extends DurableObject { if ( route === undefined || !isCurrentPreparingAttempt(route, failure.attemptId) || - route.repoKey === null || + route.environment === null || route.attemptDeadlineAt <= Date.now() || routes.some(candidate => candidate.state === 'ready') ) @@ -4743,12 +4716,6 @@ export class SandboxControlV2 extends DurableObject { private async runCloseSocket(state: AllocationState): Promise { if (state.allocationId === null || state.connectionId === null) return; - if ( - [...this.repositoryCaptures.values()].some( - capture => capture.allocationId === state.allocationId && capture.ok === undefined - ) - ) - return; await this.applyEvent({ type: 'socket-closed', at: Date.now(), @@ -4897,11 +4864,6 @@ export class SandboxControlV2 extends DurableObject { wrapperId: frame.wrapperId, }); await confirmRepositoryLaunch(this.ctx.storage, frame.allocationId); - for (const [sessionId, capture] of this.repositoryCaptures) { - if (capture.allocationId === frame.allocationId && capture.ok !== undefined) { - this.trySendFrame(ws, { type: 'workspace.captured', sessionId, ok: capture.ok }); - } - } return; } @@ -4993,7 +4955,6 @@ export class SandboxControlV2 extends DurableObject { ); return; case 'session.ready': - if (current) this.repositoryCaptures.delete(frame.sessionId); if (frame.workspace !== undefined) { logger .withFields({ sessionId: frame.sessionId, workspace: frame.workspace }) @@ -5008,7 +4969,16 @@ export class SandboxControlV2 extends DurableObject { await onRouteReady(ctx, frame.sessionId, current); return; case 'workspace.capture': - this.startRepositoryCapture(frame, currentState, current); + // Sessions never capture. A wrapper started before this change may + // still ask; it gets an immediate answer instead of waiting out its + // backstop. + if (current) { + this.trySendFrame(ws, { + type: 'workspace.captured', + sessionId: frame.sessionId, + ok: false, + }); + } return; case 'session.failed': logger @@ -5047,88 +5017,6 @@ export class SandboxControlV2 extends DurableObject { } } - /** - * Runs a wrapper's capture request off the serial queue: the snapshot of unknown - * duration must not hold up routes, frames or a stop. The wrapper always gets an - * answer, `ok: false` when nothing was saved, so it never waits out its backstop - * for a capture the DO already knows will not happen. - */ - private startRepositoryCapture( - frame: Extract, - state: AllocationState, - current: boolean - ): void { - if (!current || this.repositoryCaptures.has(frame.sessionId)) return; - this.repositoryCaptures.set(frame.sessionId, { allocationId: state.allocationId }); - this.ctx.waitUntil( - this.runRepositoryCapture(frame, state).finally(async () => { - await this.enqueue(async () => this.armAlarm(await this.readAllocation())); - }) - ); - } - - private async runRepositoryCapture( - frame: Extract, - state: AllocationState - ): Promise { - let ok = false; - let reason = 'threw'; - let repoKeyPrefix: string | undefined; - try { - const route = await readRoute(this.db, frame.sessionId); - repoKeyPrefix = route?.repoKey?.slice(0, 16) ?? undefined; - const provider = this.provider; - const timers = CONTROL_PLANE_TIMERS.wrapper; - const captureBudgetMs = - Math.max(REPOSITORY_CAPTURE_CALL_MS, timers.captureMs) + - timers.kiloRuntimeStartMs + - timers.kiloSessionMs; - if (route === null) reason = 'no_route'; - else if (route.state !== 'preparing') reason = 'route_not_preparing'; - else if (route.repoKey === null) reason = 'no_repo_key'; - else if (provider.captureRepository === undefined) reason = 'provider_no_capture'; - else if (state.providerRef === null) reason = 'no_provider_ref'; - else if (route.attemptDeadlineAt - Date.now() <= captureBudgetMs) - reason = 'insufficient_preparation_budget'; - else { - reason = 'capture_declined'; - ok = await withTimeout( - provider.captureRepository( - state.providerRef, - startupRepositoryKey( - route.repoKey, - (await readRepositoryLaunch(this.ctx.storage))?.startupMode ?? 'preinstalled' - ), - frame.commit - ), - REPOSITORY_CAPTURE_CALL_MS, - 'Repository capture timed out' - ); - if (ok) reason = 'captured'; - } - } catch { - ok = false; - reason = 'threw'; - } - logControlDiagnostic('snapshot_capture', { - sandboxId: this.sandboxId, - sessionId: frame.sessionId, - allocationId: state.allocationId, - ok, - reason, - repoKey: repoKeyPrefix, - commit: frame.commit, - }); - const latest = await this.readAllocation(); - if (latest.allocationId !== state.allocationId) return; - const capture = this.repositoryCaptures.get(frame.sessionId); - if (capture?.allocationId !== state.allocationId) return; - capture.ok = ok; - const socket = this.boundWrapperSocket(latest); - if (socket === null) return; - this.trySendFrame(socket, { type: 'workspace.captured', sessionId: frame.sessionId, ok }); - } - private releaseBoundSockets(ws: WebSocket, allocationId: string): void { for (const other of this.ctx.getWebSockets()) { if (other === ws) continue; @@ -5473,15 +5361,7 @@ export class SandboxControlV2 extends DurableObject { // The one alarm is the earliest of the allocation timers and the route // preparation deadlines, so a hung wrapper's route cannot outlive its // 12-minute attempt. - const allocationAt = nextAllocationAlarmAt( - state.kind === 'connected' && - [...this.repositoryCaptures.values()].some( - capture => capture.allocationId === state.allocationId && capture.ok === undefined - ) - ? { ...state, lastFrameAt: Date.now() } - : state, - this.sandboxTimers() - ); + const allocationAt = nextAllocationAlarmAt(state, this.sandboxTimers()); const routeAt = await earliestRouteDeadlineAt(this.db); // Pending work must survive loss of the in-memory launch continuation. const retryAt = diff --git a/services/cloud-agent-next/src/control-plane/sandbox/sqlite-schema.ts b/services/cloud-agent-next/src/control-plane/sandbox/sqlite-schema.ts index 221c854e1ac..d69b75110a5 100644 --- a/services/cloud-agent-next/src/control-plane/sandbox/sqlite-schema.ts +++ b/services/cloud-agent-next/src/control-plane/sandbox/sqlite-schema.ts @@ -32,8 +32,13 @@ export const routes = sqliteTable('routes', { // A reference to scope_grants.id, not a credential snapshot. grant: text('grant'), credential_source: text('credential_source'), - /** Keyed hash naming the repository snapshot this route may use; null when none applies. */ + /** + * Retired repository snapshot key. Kept for the rollback window; no longer + * written or read. + */ repo_key: text('repo_key'), + /** JSON of the admitted environment reference; null when the session has none. */ + environment: text('environment'), state: text('state').notNull(), attempt_id: text('attempt_id').notNull(), attempt_deadline_at: integer('attempt_deadline_at'), diff --git a/services/cloud-agent-next/src/control-plane/session/registration.ts b/services/cloud-agent-next/src/control-plane/session/registration.ts index c5127379eae..423ee61686f 100644 --- a/services/cloud-agent-next/src/control-plane/session/registration.ts +++ b/services/cloud-agent-next/src/control-plane/session/registration.ts @@ -317,5 +317,11 @@ export function buildControlPlaneSessionRegistration( ...(encryptedMcpServers === undefined ? {} : { mcpServers: encryptedMcpServers }), ...(encryptedSecrets === undefined ? {} : { encryptedSecrets }), }; - return { sandboxId, spec, credentials, sandboxSelection: { ...sandboxSelection, containment } }; + return { + sandboxId, + spec, + credentials, + sandboxSelection: { ...sandboxSelection, containment }, + ...(metadata.environment ? { environment: metadata.environment } : {}), + }; } 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..dfebef4f4f0 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 @@ -5,6 +5,7 @@ import { sandboxProviderConfigurationSchema } from '../../sandbox-control/provid import { sandboxBillingInputEnvelopeSchema } from '../../container-usage-context.js'; import { credentialContainmentSchema } from '../../sandbox-control/credential-containment.js'; import { sandboxKindSchema } from '../../sandbox-placement.js'; +import { environmentReferenceSchema } from '../../environment-build/reference.js'; import { controlPlaneCredentialSourceSchema, controlPlaneRouteSpecSchema, @@ -47,6 +48,8 @@ export const controlPlanePrepareInputWithSelectionSchema = z spec: controlPlaneRouteSpecSchema, credentials: controlPlaneCredentialSourceSchema, sandboxSelection: controlPlaneSandboxSelectionSchema.optional(), + /** The admitted repository environment; DO-private like `credentials`. */ + environment: environmentReferenceSchema.optional(), }) .strict(); 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..ee6d0240737 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 @@ -1,3 +1,4 @@ +import { environmentReferenceSchema } from '../../environment-build/reference.js'; import { DurableObject } from 'cloudflare:workers'; import { DEFAULT_DO_RETRY_CONFIG, withTimeout } from '@kilocode/worker-utils'; import { normalizeGitUrl } from '@kilocode/worker-utils'; @@ -247,6 +248,7 @@ const SETUP_EVENT_TYPES: ReadonlySet = new Set([ /** Public `PreparingStep` for a control-plane route preparation step (spec §10). */ const PREPARING_STEP_PUBLIC: Record = { sandbox_create: 'sandbox_provision', + environment_snapshot: 'environment_snapshot', sandbox_start: 'sandbox_boot', snapshot_fallback: 'snapshot_fallback', clone: 'cloning', @@ -260,6 +262,7 @@ const PREPARING_STEP_PUBLIC: Record = { const PREPARING_STEP_MESSAGE: Record = { sandbox_create: 'Creating sandbox', + environment_snapshot: 'Checking the environment snapshot', sandbox_start: 'Starting sandbox', snapshot_fallback: 'Retrying without a snapshot', clone: 'Cloning repository', @@ -311,6 +314,8 @@ export const controlPlaneSessionRegistrationSchema = z credentials: controlPlaneCredentialSourceSchema, /** Worker-selected provider pin (H1/H2); the DO applies it, never re-decides it. */ sandboxSelection: controlPlaneSandboxSelectionSchema.optional(), + /** The admitted repository environment; DO-private, for snapshot selection only. */ + environment: environmentReferenceSchema.optional(), }) .strict(); export type ControlPlaneSessionRegistration = z.infer; @@ -2724,6 +2729,7 @@ export class SandboxSessionV2 extends DurableObject { ...(registration.sandboxSelection ? { sandboxSelection: registration.sandboxSelection } : {}), + ...(registration.environment ? { environment: registration.environment } : {}), }), 'session.prepare', this.sandboxRpcConfig(pass) diff --git a/services/cloud-agent-next/src/e2e-entry.ts b/services/cloud-agent-next/src/e2e-entry.ts index 09e5df09efa..8b35aa37b62 100644 --- a/services/cloud-agent-next/src/e2e-entry.ts +++ b/services/cloud-agent-next/src/e2e-entry.ts @@ -26,6 +26,7 @@ export { export { SandboxControlV2 as SandboxControl } from './control-plane/sandbox/sandbox-do.js'; export { SandboxSessionV2 as SandboxSession } from './control-plane/session/session-do.js'; export { StreamTicketNonceDO } from './persistence/StreamTicketNonceDO.js'; +export { EnvironmentBuild } from './environment-build/environment-build-do.js'; export { UserKiloFacade } from './kilo-facade/user-kilo-facade.js'; export { E2eCallbackSink } from './persistence/E2eCallbackSink.js'; diff --git a/services/cloud-agent-next/src/environment-build/access.ts b/services/cloud-agent-next/src/environment-build/access.ts new file mode 100644 index 00000000000..284752cbb40 --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/access.ts @@ -0,0 +1,143 @@ +import type { WorkerDb } from '@kilocode/db/client'; +import { organization_memberships } from '@kilocode/db/schema'; +import { and, eq } from 'drizzle-orm'; +import { + resolveCloudAgentGitHubAuthForRepo, + resolveManagedBitbucketToken, + resolveManagedGitLabToken, +} from '../services/git-token-service-client.js'; +import type { Env } from '../types.js'; +import type { EnvironmentBuildInput } from '../shared/environment-build-protocol.js'; +import type { SessionEnvironmentReference } from './reference.js'; + +/** + * Who may do what with an environment's builds (plan "Build admission and + * execution"). PostgreSQL is authoritative; the coordinator rechecks these + * before allocation and again before publication. + */ + +/** Organization roles with environment-management authority (`@kilocode/app-shared`). */ +const ENVIRONMENT_MANAGE_ROLES: ReadonlySet = new Set(['owner', 'admin']); + +export type EnvironmentActorAccess = 'manage' | 'read' | 'none'; + +/** + * A personal environment is managed only by its owner. An organization + * environment is managed by owners and admins and readable by every member. + */ +export async function environmentActorAccess( + db: WorkerDb, + owner: SessionEnvironmentReference['owner'], + userId: string +): Promise { + if (owner.type === 'user') return owner.id === userId ? 'manage' : 'none'; + const [membership] = await db + .select({ role: organization_memberships.role }) + .from(organization_memberships) + .where( + and( + eq(organization_memberships.organization_id, owner.id), + eq(organization_memberships.kilo_user_id, userId) + ) + ) + .limit(1); + if (!membership) return 'none'; + return ENVIRONMENT_MANAGE_ROLES.has(membership.role) ? 'manage' : 'read'; +} + +export type BuilderGitSource = + | { ok: true; git: EnvironmentBuildInput['git'] } + | { ok: false; reason: 'repository_unauthorized' | 'unavailable' }; + +const TRANSIENT_REASONS: ReadonlySet = new Set([ + 'service_not_configured', + 'rpc_error', + 'temporarily_unavailable', +]); + +/** + * A short-lived, repository-scoped Git source for the initiating actor. A + * resolved token is also the repository authorization check: the integration + * must still grant the actor's owner access to this repository. + */ +export async function resolveBuilderGitSource( + env: Pick< + Env, + | 'GIT_TOKEN_SERVICE' + | 'GITHUB_APP_SLUG' + | 'GITHUB_LITE_APP_SLUG' + | 'GITHUB_APP_BOT_USER_ID' + | 'GITHUB_LITE_APP_BOT_USER_ID' + >, + input: { reference: SessionEnvironmentReference; repoFullName: string; actorUserId: string } +): Promise { + const { reference, actorUserId } = input; + const orgId = reference.owner.type === 'organization' ? reference.owner.id : undefined; + const repository = reference.repository; + switch (repository.platform) { + case 'github': { + const resolved = await resolveCloudAgentGitHubAuthForRepo(env, { + githubRepo: input.repoFullName, + userId: actorUserId, + ...(orgId ? { orgId } : {}), + accessPurpose: 'workflow', + allowUserAuthorization: false, + }); + if (!resolved.success) { + return { + ok: false, + reason: TRANSIENT_REASONS.has(resolved.error.reason) + ? 'unavailable' + : 'repository_unauthorized', + }; + } + return { + ok: true, + git: { + url: `https://github.com/${input.repoFullName}.git`, + token: resolved.value.githubToken, + platform: 'github', + }, + }; + } + case 'gitlab': { + const resolved = await resolveManagedGitLabToken(env, { + userId: actorUserId, + ...(orgId ? { orgId } : {}), + repositoryUrl: repository.cloneUrl, + }); + if (!resolved.success) { + return { + ok: false, + reason: TRANSIENT_REASONS.has(resolved.reason) + ? 'unavailable' + : 'repository_unauthorized', + }; + } + return { + ok: true, + git: { url: repository.cloneUrl, token: resolved.token, platform: 'gitlab' }, + }; + } + case 'bitbucket': { + if (!orgId) return { ok: false, reason: 'repository_unauthorized' }; + const url = `https://bitbucket.org/${input.repoFullName}.git`; + const resolved = await resolveManagedBitbucketToken(env, { + userId: actorUserId, + orgId, + workspaceUuid: repository.workspaceUuid, + repositoryUuid: repository.repositoryUuid, + repositoryUrl: url, + }); + if (!resolved.success) { + return { + ok: false, + reason: TRANSIENT_REASONS.has(resolved.reason) + ? 'unavailable' + : 'repository_unauthorized', + }; + } + return { ok: true, git: { url, token: resolved.token, platform: 'bitbucket' } }; + } + } +} diff --git a/services/cloud-agent-next/src/environment-build/builder-provider.test.ts b/services/cloud-agent-next/src/environment-build/builder-provider.test.ts new file mode 100644 index 00000000000..47ff335e2fa --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/builder-provider.test.ts @@ -0,0 +1,94 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Env } from '../types.js'; +import type { + ContainersBuilderLaunchInput, + SandboxContainers, +} from '../sandbox-containers/SandboxContainers.js'; +import { downloadedStartup } from '../sandbox-control/downloaded-startup.js'; +import { builderInstance, createBuilderProvider } from './builder-provider.js'; + +vi.mock('../sandbox-control/downloaded-startup.js', () => ({ downloadedStartup: vi.fn() })); +const allocation = { + sandboxId: 'sbx-builder', + allocationId: 'allocation', + instance: 'standard-4' as const, +}; +const runner = { url: 'https://worker.test/environment-build/test', credential: 'synthetic' }; +const command = { argv: ['/bin/sh', '-c', 'download-selected-builder'] }; + +function harness(env: Partial) { + const launchBuilder = vi.fn(async (_input: ContainersBuilderLaunchInput) => ({ + started: true, + startSource: 'image' as const, + })); + const ensureLeaseAtLeast = vi.fn(async () => undefined); + const containers = { + getByName: () => ({ launchBuilder, ensureLeaseAtLeast }), + } as unknown as DurableObjectNamespace; + const provider = createBuilderProvider({ + containers, + env: { WORKER_URL: 'https://worker.test', ...env } as Env, + }); + return { provider, launchBuilder }; +} + +beforeEach(() => vi.mocked(downloadedStartup).mockReset().mockResolvedValue(command)); + +describe('environment builder startup selection', () => { + it('uses setup size only when setup commands exist', () => { + expect(builderInstance(['install'])).toBe('standard-4'); + expect(builderInstance([])).toBe('standard-3'); + }); + + it('keeps preinstalled startup even when a release descriptor exists', async () => { + const { provider, launchBuilder } = harness({ + RUNTIME_RELEASE_DESCRIPTOR: 'available-release', + }); + await provider.launch(allocation, { runner, deadlineAt: 100 }); + expect(downloadedStartup).not.toHaveBeenCalled(); + expect(launchBuilder.mock.calls[0]?.[0]).toMatchObject({ + command: { argv: ['/usr/local/bin/bun', '/usr/local/bin/kilocode-environment-builder.js'] }, + runner, + }); + expect(await provider.runtimeReleaseId()).toBeNull(); + }); + + it.each(['all-in-one', 'runtime-free'] as const)( + 'uses the current installer for the %s image', + async image => { + const { provider, launchBuilder } = harness({ + RUNTIME_STARTUP_MODE: 'downloaded', + NATIVE_RUNTIME_IMAGE: image, + RUNTIME_RELEASE_DESCRIPTOR: 'release', + }); + await provider.launch(allocation, { runner, deadlineAt: 100 }); + expect(downloadedStartup).toHaveBeenCalledWith( + expect.objectContaining({ + descriptor: 'release', + sandboxId: allocation.sandboxId, + allocationId: allocation.allocationId, + deadlineAt: 100, + mainProcess: 'environment-builder', + }) + ); + expect(launchBuilder.mock.calls[0]?.[0]).toMatchObject({ command, runner, deadlineAt: 100 }); + } + ); + + it('keeps an existing descriptor digest as the release identity', async () => { + const digest = 'a'.repeat(64); + const { provider } = harness({ + RUNTIME_STARTUP_MODE: 'downloaded', + RUNTIME_RELEASE_DESCRIPTOR: digest, + }); + expect(await provider.runtimeReleaseId()).toBe(digest); + }); + + it('rejects runtime-free startup without a downloadable release before starting a builder', async () => { + const { provider, launchBuilder } = harness({ NATIVE_RUNTIME_IMAGE: 'runtime-free' }); + await expect(provider.launch(allocation, { runner, deadlineAt: 100 })).rejects.toMatchObject({ + code: 'invalid_configuration', + }); + expect(launchBuilder).not.toHaveBeenCalled(); + }); +}); diff --git a/services/cloud-agent-next/src/environment-build/builder-provider.ts b/services/cloud-agent-next/src/environment-build/builder-provider.ts new file mode 100644 index 00000000000..733a9ea2727 --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/builder-provider.ts @@ -0,0 +1,153 @@ +import { + CLOUDFLARE_CONTAINERS_DEFAULT_INSTANCE, + type CloudflareContainersInstance, +} from '@kilocode/worker-utils/sandbox-allocation'; +import { hashToSandboxId } from '../sandbox-id.js'; +import type { EnvironmentBuildRunner } from '../shared/environment-build-protocol.js'; +import type { SandboxBillingInput } from '../container-usage-context.js'; +import { createCloudflareContainersProviderAdapter } from '../sandbox-control/cloudflare-containers-provider.js'; +import { encodeCloudflareProviderRef } from '../sandbox-control/cloudflare-provider.js'; +import { leaseAtLeastMs } from '../sandbox-control/deadlines.js'; +import { downloadedStartup } from '../sandbox-control/downloaded-startup.js'; +import { selectStartup } from '../sandbox-control/startup-selection.js'; +import { SHA256_PATTERN } from '../sandbox-control/runtime-release.js'; +import type { Env } from '../types.js'; +import type { + NativeCaptureResult, + SandboxContainers, +} from '../sandbox-containers/SandboxContainers.js'; +import type { ProviderAdapter } from '../sandbox-control/provider.js'; + +/** + * Physical builder lifecycle over the native Containers primitives (plan "Use a + * dedicated builder runner ... Reuse ... provider lifecycle primitives"). The + * builder container DO is named for its environment, so a builder can never + * share a container DO with a session. Billing admission, stop and lease reuse + * the session provider adapter; only the start uses the builder entry point. + */ + +export type BuilderAllocation = { + /** The container DO name and the billing sandbox key (`sbx-` placed key, isolated kind). */ + sandboxId: string; + allocationId: string; + instance: CloudflareContainersInstance; +}; + +export type BuilderLaunch = { + /** Delivered to the running builder by exec, never as container start env. */ + runner: EnvironmentBuildRunner; + deadlineAt: number; +}; + +export type BuilderProvider = { + /** Container billing admission and attribution; throws `ProviderCreationError` when refused. */ + admitBilling(allocation: BuilderAllocation, billing: SandboxBillingInput): Promise; + /** Start or confirm the builder with the deployment startup selection. */ + launch(allocation: BuilderAllocation, launch: BuilderLaunch): Promise; + capture(allocation: BuilderAllocation, captureId: string): Promise; + stop(allocation: BuilderAllocation): Promise<'terminal' | 'retryable'>; + renewLease(allocation: BuilderAllocation): Promise; + /** SHA-256 of the selected runtime release descriptor, or null for the baked image. */ + runtimeReleaseId(): Promise; + /** The image a container in `sandboxId` starts from now, or null when unreadable. */ + currentImage(sandboxId: string): Promise; +}; + +/** The one builder container DO name for an environment. */ +export function builderSandboxId(environmentId: string): Promise { + return hashToSandboxId(`environment-build:v1:${environmentId}`, 'sbx'); +} + +/** `standard-4` when setup commands exist, otherwise the default native instance. */ +export function builderInstance(commands: readonly string[]): CloudflareContainersInstance { + return commands.length > 0 ? 'standard-4' : CLOUDFLARE_CONTAINERS_DEFAULT_INSTANCE; +} + +function providerRef(allocation: BuilderAllocation): string { + return encodeCloudflareProviderRef({ + sandboxId: allocation.sandboxId, + containment: false, + instanceId: allocation.allocationId, + }); +} + +async function sha256Hex(value: string): Promise { + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)); + return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join(''); +} + +export function createBuilderProvider(deps: { + containers: DurableObjectNamespace; + env: Pick< + Env, + | 'RUNTIME_STARTUP_MODE' + | 'NATIVE_RUNTIME_IMAGE' + | 'RUNTIME_RELEASE_DESCRIPTOR' + | 'WORKER_URL' + | 'NEXTAUTH_SECRET' + | 'RUNTIME_RELEASE_BUCKET' + >; +}): BuilderProvider { + const adapterFor = (allocation: BuilderAllocation): ProviderAdapter => + createCloudflareContainersProviderAdapter({ + logicalSandboxId: allocation.sandboxId, + allocationName: allocation.sandboxId, + instance: allocation.instance, + getContainer: id => deps.containers.getByName(id), + }); + const stub = (allocation: BuilderAllocation) => deps.containers.getByName(allocation.sandboxId); + + return { + async admitBilling(allocation, billing) { + await adapterFor(allocation).ensureBillingAdmission(providerRef(allocation), billing); + }, + async launch(allocation, launch) { + const selection = selectStartup(deps.env, 'cloudflare-containers'); + const command = + selection.mode === 'downloaded' + ? await downloadedStartup({ + env: deps.env, + descriptor: selection.descriptor, + sandboxId: allocation.sandboxId, + allocationId: allocation.allocationId, + deadlineAt: launch.deadlineAt, + mainProcess: 'environment-builder', + }) + : { argv: ['/usr/local/bin/bun', '/usr/local/bin/kilocode-environment-builder.js'] }; + const ref = providerRef(allocation); + await stub(allocation).launchBuilder({ + allocationRef: ref, + runner: launch.runner, + instance: allocation.instance, + deadlineAt: launch.deadlineAt, + command, + }); + await stub(allocation).ensureLeaseAtLeast(ref, leaseAtLeastMs()); + }, + capture(allocation, captureId) { + return stub(allocation).captureSnapshot(providerRef(allocation), captureId); + }, + stop(allocation) { + return adapterFor(allocation).stop(providerRef(allocation)); + }, + async renewLease(allocation) { + await adapterFor(allocation).ensureLeaseAtLeast(providerRef(allocation), leaseAtLeastMs()); + }, + async runtimeReleaseId() { + const selection = selectStartup(deps.env, 'cloudflare-containers'); + const descriptor = selection.mode === 'downloaded' ? selection.descriptor : undefined; + return descriptor === undefined + ? null + : SHA256_PATTERN.test(descriptor) + ? descriptor + : sha256Hex(descriptor); + }, + async currentImage(sandboxId) { + try { + return await deps.containers.getByName(sandboxId).currentImage(); + } catch { + return null; + } + }, + }; +} diff --git a/services/cloud-agent-next/src/environment-build/coordinator.ts b/services/cloud-agent-next/src/environment-build/coordinator.ts new file mode 100644 index 00000000000..4257660822d --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/coordinator.ts @@ -0,0 +1,1183 @@ +import type { + EnvironmentBuildInput as ProfileBuildInput, + EnvironmentReference, +} from '@kilocode/cloud-agent-profile'; +import type { SandboxBillingInput } from '../container-usage-context.js'; +import { ProviderCreationError } from '../sandbox-control/provider.js'; +import { RUNTIME_BASE_ABI } from '../sandbox-control/runtime-release.js'; +import { + ENVIRONMENT_BUILD_WORKSPACE, + type EnvironmentBuildEvents, + type EnvironmentBuildEventsReply, + type EnvironmentBuildInput, + type EnvironmentBuildResult, +} from '../shared/environment-build-protocol.js'; +import type { + EnvironmentBuildCancelResult, + EnvironmentBuildLogs, + EnvironmentBuildRequestResult, + EnvironmentBuildStatus, + EnvironmentBuildSummary, +} from '../shared/environment-build-status.js'; +import type { EnvironmentActorAccess, BuilderGitSource } from './access.js'; +import { + builderInstance, + type BuilderAllocation, + type BuilderProvider, +} from './builder-provider.js'; +import { + BUILDER_FIRST_REPORT_MS, + BUILDER_REPORT_LOSS_MS, + BUILD_DEADLINE_MS, + CLEANUP_ATTEMPTS_BEFORE_FAILURE, + COUNTED_FAILURE_REASONS, + PREPARING_TICK_MS, + SESSION_PREPARATION_BUDGET_MS, + SNAPSHOT_ELIGIBLE_LIFETIME_MS, + automaticAdmission, + cleanupRetryDelay, + compatibleWith, + replacementNeeded, + snapshotEligibility, + type SnapshotCompatibility, + type PublishedSnapshot, +} from './policy.js'; +import { sameEnvironmentScope, type SessionEnvironmentReference } from './reference.js'; +import type { BuildOutcome, BuildState, BuildTrigger } from './sqlite-schema.js'; +import { + isTerminal, + parseBuildInput, + withStage, + type BuildInputRecord, + type BuildRow, + type BuildStore, +} from './store.js'; + +/** + * The environment build coordinator (plan "Build admission and execution"). One + * instance per environment, inside its `EnvironmentBuild` Durable Object. It + * owns admission, deduplication, deadlines, the alarm-driven execution state, + * publication and cleanup. Every provider call happens outside any storage + * transaction, and every transition re-checks the state it expects, because + * RPCs from the builder and the Sandbox DO interleave with alarm work. + */ + +export type CoordinatorEnvironment = { + reference(environmentId: string): Promise; + buildInput(environmentId: string): Promise; + actorAccess( + owner: EnvironmentReference['owner'], + userId: string + ): Promise; + gitSource(input: { + reference: SessionEnvironmentReference; + repoFullName: string; + actorUserId: string; + }): Promise; + /** Owner-level container concurrency, the limit sessions use. */ + capacity( + owner: EnvironmentReference['owner'], + sandboxId: string, + actorUserId: string + ): Promise<'ok' | 'limit_reached'>; +}; + +export type CoordinatorDeps = { + now(): number; + uuid(): string; + store: BuildStore; + /** Transactional write of several store calls. */ + transaction(write: () => T): T; + /** Set the DO alarm no later than `at`. */ + armAlarm(at: number): Promise; + environment: CoordinatorEnvironment; + provider: BuilderProvider; + /** The builder's control base URL for `buildId`, or null when the Worker URL is unknown. */ + runnerUrl(environmentId: string, buildId: string): string | null; + /** The signed runner credential bound to this build and allocation. */ + runnerCredential(claims: { + environmentId: string; + buildId: string; + allocationId: string; + }): Promise; + /** True when `credential` is this build and allocation's derived credential. */ + runnerCredentialMatches( + claims: { environmentId: string; buildId: string; allocationId: string }, + credential: string + ): Promise; + decryptSecrets(encrypted: Record): Record; + billingInput(input: { + owner: EnvironmentReference['owner']; + actorUserId: string; + sandboxId: string; + buildId: string; + }): SandboxBillingInput; + /** The builder container DO name of this environment (`builderSandboxId`). */ + builderSandboxId: string; + /** Null when snapshot builds can run for this owner; otherwise why not. */ + unsupportedReason(owner: EnvironmentReference['owner']): string | null; + log(message: string, fields: Record): void; +}; + +export type RunnerAuth = { + environmentId: string; + buildId: string; + allocationId: string; + credential: string; +}; + +export type RunnerRefusal = { refused: 'unauthorized' | 'gone' | 'unavailable' }; + +export type SnapshotAdmission = + | { status: 'admitted' | 'active'; buildId: string } + | { + status: 'skipped'; + reason: + | 'environment_deleted' + | 'revision_superseded' + | 'build_in_progress' + | 'cleanup_pending' + | 'backoff' + | 'container_limit_reached' + | 'unsupported' + | 'unavailable'; + retryAt?: number; + } + | { status: 'not_needed' }; + +export type SnapshotResolution = { + snapshot: { + snapshotId: string; + baseImage: string; + revision: number; + capturedAt: number; + buildId: string; + stale: 'revision' | 'age' | null; + } | null; + /** Why no snapshot was selected, when none was. */ + reason: + | 'eligible' + | 'missing' + | 'scope_mismatch' + | 'incompatible' + | 'newer_than_session' + | 'expired' + | 'deleted' + | 'unsupported'; + admission: SnapshotAdmission; +}; + +const ACTIVE_RUNNER_STATES: readonly BuildState[] = ['preparing']; +const CAPTURE_POLL_MS = 15_000; +const LEASE_RENEW_INTERVAL_MS = 60_000; + +export class EnvironmentBuildCoordinator { + private lastLeaseRenewalAt = 0; + + constructor( + private readonly environmentId: string, + private readonly deps: CoordinatorDeps + ) {} + + // --- admission ------------------------------------------------------------- + + async requestBuild(request: { + owner: EnvironmentReference['owner']; + expectedRevision: number; + requestId: string; + trigger: BuildTrigger; + actorUserId: string; + }): Promise { + const { store } = this.deps; + store.bindEnvironment(this.environmentId); + if (this.deps.unsupportedReason(request.owner) !== null) { + return { status: 'rejected', reason: 'unsupported_provider' }; + } + if (store.environment()?.deletedAt != null) return { status: 'rejected', reason: 'not_found' }; + const replay = store.buildByRequest(request.requestId); + if (replay !== null) return { status: 'accepted', buildId: replay.id, created: false }; + const active = store.activeBuild(); + if (active !== null) { + if (active.state === 'stopping') { + return { status: 'conflict', reason: 'cleanup_pending', buildId: active.id }; + } + if (active.revision === request.expectedRevision) { + return { status: 'accepted', buildId: active.id, created: false }; + } + return { status: 'conflict', reason: 'build_in_progress', buildId: active.id }; + } + const reference = await this.deps.environment.reference(this.environmentId); + if (reference === null) { + store.markDeleted(this.deps.now()); + return { status: 'rejected', reason: 'not_found' }; + } + if (reference.configurationRevision !== request.expectedRevision) { + return { status: 'conflict', reason: 'revision_changed', buildId: null }; + } + const sandboxId = this.deps.builderSandboxId; + if ( + (await this.deps.environment.capacity(reference.owner, sandboxId, request.actorUserId)) === + 'limit_reached' + ) { + return { status: 'rejected', reason: 'container_limit_reached' }; + } + const input = await this.deps.environment.buildInput(this.environmentId); + if (input === null) { + store.markDeleted(this.deps.now()); + return { status: 'rejected', reason: 'not_found' }; + } + if (input.reference.configurationRevision !== request.expectedRevision) { + return { status: 'conflict', reason: 'revision_changed', buildId: null }; + } + const buildId = await this.admit( + input, + request.requestId, + request.trigger, + request.actorUserId + ); + if (buildId === null) { + // Another request admitted a build between the read and the insert. + const raced = store.activeBuild(); + return raced !== null && raced.revision === request.expectedRevision + ? { status: 'accepted', buildId: raced.id, created: false } + : { status: 'conflict', reason: 'build_in_progress', buildId: raced?.id ?? null }; + } + return { status: 'accepted', buildId, created: true }; + } + + /** + * Persist a queued build and its alarm before acknowledging (plan: "The build + * coordinator commits the queued record and its next alarm before + * acknowledging the request"). Returns null when another build became active. + */ + private async admit( + input: ProfileBuildInput, + requestId: string, + trigger: BuildTrigger, + actorUserId: string + ): Promise { + const now = this.deps.now(); + const buildId = this.deps.uuid(); + const record: BuildInputRecord = { + reference: input.reference, + repoFullName: input.repoFullName, + commands: input.commands, + plainVars: input.plainVars, + }; + const runtimeRelease = await this.deps.provider.runtimeReleaseId(); + const admitted = this.deps.transaction(() => { + if (this.deps.store.activeBuild() !== null) return false; + this.deps.store.insertBuild({ + id: buildId, + request_id: requestId, + revision: input.reference.configurationRevision, + trigger, + actor_user_id: actorUserId, + input: JSON.stringify(record), + encrypted_secrets: JSON.stringify(input.encryptedSecrets), + instance: builderInstance(input.commands), + runtime_release: runtimeRelease, + state: 'queued', + cleanup_attempts: 0, + log_seq: 0, + log_bytes: 0, + log_truncated: false, + stage_times: JSON.stringify({ queued: now }), + created_at: now, + deadline_at: now + BUILD_DEADLINE_MS, + }); + if (trigger === 'session') { + this.deps.store.recordAutoAdmitted(input.reference.configurationRevision, now); + } + return true; + }); + if (!admitted) return null; + await this.deps.armAlarm(now); + this.deps.log('Environment build admitted', { + environmentId: this.environmentId, + buildId, + revision: input.reference.configurationRevision, + trigger, + }); + return buildId; + } + + /** + * Session launch lookup (plan "Session launch"). It reads snapshot metadata and + * may admit one background build; it never waits for build execution. + */ + async resolveSnapshot(request: { + reference: SessionEnvironmentReference; + actorUserId: string; + }): Promise { + if (this.deps.unsupportedReason(request.reference.owner) !== null) { + return { + snapshot: null, + reason: 'unsupported', + admission: { status: 'skipped', reason: 'unsupported' }, + }; + } + const { store } = this.deps; + store.bindEnvironment(this.environmentId); + const now = this.deps.now(); + if (store.environment()?.deletedAt != null) { + return { + snapshot: null, + reason: 'deleted', + admission: { status: 'skipped', reason: 'environment_deleted' }, + }; + } + const published = store.published(); + const current = await this.compatibility(); + const eligibility = snapshotEligibility(published, request.reference, now, current); + const snapshot = + eligibility.eligible && published !== null + ? { + snapshotId: published.snapshotId, + baseImage: published.baseImage, + revision: published.revision, + capturedAt: published.capturedAt, + buildId: published.buildId, + stale: eligibility.stale ? eligibility.staleReason : null, + } + : null; + const reason = eligibility.eligible ? 'eligible' : eligibility.reason; + const admission = replacementNeeded( + published, + request.reference.configurationRevision, + now, + current + ) + ? await this.admitAutomatic(request.reference, request.actorUserId) + : ({ status: 'not_needed' } as const); + return { snapshot, reason, admission }; + } + + private async admitAutomatic( + reference: SessionEnvironmentReference, + actorUserId: string + ): Promise { + const { store } = this.deps; + if (this.deps.unsupportedReason(reference.owner) !== null) { + return { status: 'skipped', reason: 'unsupported' }; + } + const active = store.activeBuild(); + if (active !== null) { + if (active.state === 'stopping') return { status: 'skipped', reason: 'cleanup_pending' }; + return active.revision === reference.configurationRevision + ? { status: 'active', buildId: active.id } + : { status: 'skipped', reason: 'build_in_progress' }; + } + const backoff = automaticAdmission( + store.autoAttempt(reference.configurationRevision), + this.deps.now() + ); + if (!backoff.admit) return { status: 'skipped', reason: 'backoff', retryAt: backoff.retryAt }; + const current = await this.deps.environment.reference(this.environmentId); + if (current === null) { + store.markDeleted(this.deps.now()); + return { status: 'skipped', reason: 'environment_deleted' }; + } + // Only the environment's current revision is ever built. + if ( + current.configurationRevision !== reference.configurationRevision || + !sameEnvironmentScope(current, reference) + ) { + return { status: 'skipped', reason: 'revision_superseded' }; + } + const sandboxId = this.deps.builderSandboxId; + if ( + (await this.deps.environment.capacity(current.owner, sandboxId, actorUserId)) === + 'limit_reached' + ) { + return { status: 'skipped', reason: 'container_limit_reached' }; + } + const input = await this.deps.environment.buildInput(this.environmentId); + if ( + input === null || + input.reference.configurationRevision !== reference.configurationRevision + ) { + return { status: 'skipped', reason: 'revision_superseded' }; + } + const buildId = await this.admit( + input, + `session:${reference.configurationRevision}:${this.deps.uuid()}`, + 'session', + actorUserId + ); + if (buildId === null) { + const raced = store.activeBuild(); + return raced !== null && raced.revision === reference.configurationRevision + ? { status: 'active', buildId: raced.id } + : { status: 'skipped', reason: 'build_in_progress' }; + } + return { status: 'admitted', buildId }; + } + + // --- reads ----------------------------------------------------------------- + + /** The image and runtime ABI a session would start with now. */ + private async compatibility(): Promise { + return { + baseImage: await this.deps.provider.currentImage(this.deps.builderSandboxId), + runtimeAbi: RUNTIME_BASE_ABI, + runtimeMode: + (await this.deps.provider.runtimeReleaseId()) === null ? 'preinstalled' : 'downloaded', + }; + } + + async status( + currentRevision: number, + owner: EnvironmentReference['owner'] + ): Promise { + const { store } = this.deps; + const now = this.deps.now(); + const current = await this.compatibility(); + const active = store.activeBuild(); + const latest = store.latestTerminal(); + const published = store.published(); + const attempt = store.autoAttempt(currentRevision); + const backoff = automaticAdmission(attempt, now); + const unsupported = this.deps.unsupportedReason(owner); + return { + environmentId: this.environmentId, + supported: unsupported === null, + unsupportedReason: unsupported, + configurationRevision: currentRevision, + activeBuild: active === null ? null : summarize(active), + latestBuild: latest === null ? null : summarize(latest), + published: + published === null ? null : summarizeSnapshot(published, currentRevision, now, current), + automaticBackoff: + attempt !== null && attempt.failures > 0 && !backoff.admit + ? { + revision: currentRevision, + failures: attempt.failures, + nextAttemptAt: backoff.retryAt, + } + : null, + sessionPreparationBudgetMs: SESSION_PREPARATION_BUDGET_MS, + }; + } + + logs(buildId: string, cursor: number | undefined, limit: number): EnvironmentBuildLogs | null { + const build = this.deps.store.build(buildId); + if (build === null) return null; + const lines = + cursor === undefined + ? this.deps.store.readRecentLogs(buildId, limit) + : this.deps.store.readLogs(buildId, cursor, limit); + const nextCursor = + lines.length === 0 ? (cursor ?? 0) : (lines[lines.length - 1]?.seq ?? cursor ?? 0); + return { + buildId, + lines, + nextCursor, + truncated: build.log_truncated, + complete: isTerminal(build.state) && nextCursor >= build.log_seq, + }; + } + + // --- control --------------------------------------------------------------- + + async cancel(buildId: string): Promise { + const now = this.deps.now(); + const queued = this.deps.store.transition(buildId, ['queued'], { + state: 'canceled', + outcome: 'canceled', + encrypted_secrets: null, + finished_at: now, + }); + if (queued !== null) return { status: 'canceled' }; + const stopping = this.beginStopping( + buildId, + ['preparing', 'capturing', 'publishing'], + 'canceled', + { + reason: 'canceled', + } + ); + if (stopping === null) return { status: 'not_active' }; + await this.deps.armAlarm(now); + return { status: 'canceling' }; + } + + /** + * Environment deletion: stop admission at once and stop an active build. Only + * PostgreSQL decides that the environment is gone; a notice for an existing + * environment changes nothing. + */ + async environmentDeleted(): Promise { + const { store } = this.deps; + store.bindEnvironment(this.environmentId); + if ((await this.deps.environment.reference(this.environmentId)) !== null) return; + store.markDeleted(this.deps.now()); + const active = store.activeBuild(); + if (active === null) return; + if (active.state === 'queued') { + store.transition(active.id, ['queued'], { + state: 'obsolete', + outcome: 'obsolete', + failure_reason: 'environment_deleted', + encrypted_secrets: null, + finished_at: this.deps.now(), + }); + return; + } + this.beginStopping(active.id, ['preparing', 'capturing', 'publishing'], 'obsolete', { + reason: 'environment_deleted', + }); + await this.deps.armAlarm(this.deps.now()); + } + + // --- runner callbacks ------------------------------------------------------ + + private async authorizeRunner( + auth: RunnerAuth, + states: readonly BuildState[] + ): Promise { + if (auth.environmentId !== this.environmentId) return { refused: 'unauthorized' }; + const build = this.deps.store.build(auth.buildId); + if (build === null || build.allocation_id !== auth.allocationId) { + return { refused: 'unauthorized' }; + } + if (!(await this.deps.runnerCredentialMatches(auth, auth.credential))) { + return { refused: 'unauthorized' }; + } + if (!states.includes(build.state)) return { refused: 'gone' }; + return build; + } + + /** The builder's inputs. Secrets are decrypted only into this reply. */ + async runnerInput(auth: RunnerAuth): Promise { + const build = await this.authorizeRunner(auth, ACTIVE_RUNNER_STATES); + if ('refused' in build) return build; + const input = parseBuildInput(build); + const git = await this.deps.environment.gitSource({ + reference: input.reference, + repoFullName: input.repoFullName, + actorUserId: build.actor_user_id, + }); + if (!git.ok) { + if (git.reason === 'unavailable') return { refused: 'unavailable' }; + await this.failRunning(build.id, 'repository_unauthorized'); + return { refused: 'gone' }; + } + let secrets: Record; + try { + secrets = + build.encrypted_secrets === null + ? {} + : this.deps.decryptSecrets( + JSON.parse(build.encrypted_secrets) as Record + ); + } catch { + await this.failRunning(build.id, 'input_unavailable', 'secrets could not be decrypted'); + return { refused: 'gone' }; + } + return { + environmentId: this.environmentId, + buildId: build.id, + allocationId: auth.allocationId, + git: git.git, + directory: ENVIRONMENT_BUILD_WORKSPACE, + commands: input.commands, + env: { ...input.plainVars, ...secrets }, + secretKeys: Object.keys(secrets), + deadlineAt: build.deadline_at, + }; + } + + async runnerEvents( + auth: RunnerAuth, + events: EnvironmentBuildEvents + ): Promise { + const build = await this.authorizeRunner(auth, ['preparing', 'stopping']); + if ('refused' in build) return build; + if (build.state === 'stopping') return { action: 'cancel' }; + const now = this.deps.now(); + const fresh = build.last_event_sequence === null || events.sequence > build.last_event_sequence; + this.deps.transaction(() => { + if (fresh && events.logs !== undefined && events.logs.length > 0) { + this.deps.store.appendLogs(build, events.logs); + } + this.deps.store.update(build.id, { + last_heartbeat_at: now, + ...(fresh ? { last_event_sequence: events.sequence } : {}), + ...(fresh && events.step !== undefined ? { runner_step: events.step } : {}), + }); + }); + if (now - this.lastLeaseRenewalAt >= LEASE_RENEW_INTERVAL_MS) { + this.lastLeaseRenewalAt = now; + await this.deps.provider.renewLease(this.allocationOf(build)).catch(() => undefined); + } + return { action: 'continue' }; + } + + async runnerResult( + auth: RunnerAuth, + result: EnvironmentBuildResult + ): Promise<'ok' | RunnerRefusal> { + const build = await this.authorizeRunner(auth, [ + 'preparing', + 'capturing', + 'publishing', + 'stopping', + ]); + if ('refused' in build) return build; + // A replayed result after the first was applied is acknowledged once more. + if (build.state !== 'preparing') return 'ok'; + const now = this.deps.now(); + if (result.outcome === 'ready') { + const captured = this.deps.store.transition(build.id, ['preparing'], { + state: 'capturing', + source_commit: result.commit, + source_branch: result.branch, + setup_duration_ms: result.setupDurationMs, + capture_id: this.deps.uuid(), + last_heartbeat_at: now, + stage_times: withStage(build, 'capturing', now), + }); + if (captured !== null) await this.deps.armAlarm(now); + return 'ok'; + } + await this.failRunning(build.id, result.reason, result.detail); + return 'ok'; + } + + // --- execution --------------------------------------------------------------- + + /** One alarm pass: advance the active build, retry due cleanups, prune history. */ + async alarm(): Promise { + const { store } = this.deps; + const now = this.deps.now(); + const active = store.activeBuild(); + if (active !== null) await this.advance(active); + for (const stopping of store.dueCleanups(this.deps.now())) { + if (stopping.id !== active?.id) await this.advanceStopping(stopping); + } + store.prune(now); + const next = this.nextAlarm(); + if (next !== null) await this.deps.armAlarm(next); + } + + private nextAlarm(): number | null { + const now = this.deps.now(); + const active = this.deps.store.activeBuild(); + if (active === null) return null; + switch (active.state) { + case 'queued': + return Math.min(now + PREPARING_TICK_MS, active.deadline_at); + case 'publishing': + return now; + case 'preparing': + return Math.min(now + PREPARING_TICK_MS, active.deadline_at); + case 'capturing': + return now + CAPTURE_POLL_MS; + case 'stopping': + return active.next_cleanup_at ?? now; + default: + return null; + } + } + + private async advance(build: BuildRow): Promise { + if ( + build.state !== 'stopping' && + this.deps.unsupportedReason(parseBuildInput(build).reference.owner) !== null + ) { + if (build.state === 'queued') { + this.terminalWithoutAllocation(build, 'obsolete', 'snapshots_disabled'); + } else { + this.beginStopping(build.id, [build.state], 'obsolete', { reason: 'snapshots_disabled' }); + } + return; + } + switch (build.state) { + case 'queued': + return this.advanceQueued(build); + case 'preparing': + return this.advancePreparing(build); + case 'capturing': + return this.advanceCapturing(build); + case 'publishing': + return this.advancePublishing(build); + case 'stopping': + return this.advanceStopping(build); + default: + return; + } + } + + private terminalWithoutAllocation( + build: BuildRow, + outcome: BuildOutcome, + reason: string, + detail?: string + ): void { + const now = this.deps.now(); + const done = this.deps.store.transition(build.id, ['queued'], { + state: outcome, + outcome, + failure_reason: reason, + failure_detail: detail ?? null, + encrypted_secrets: null, + finished_at: now, + }); + if (done !== null) this.recordAutomaticOutcome(done); + this.deps.log('Environment build ended before allocation', { + environmentId: this.environmentId, + buildId: build.id, + outcome, + reason, + }); + } + + /** Execution order steps 1 to 4: recheck, persist the allocation intent, start the builder. */ + private async advanceQueued(build: BuildRow): Promise { + const now = this.deps.now(); + if (now >= build.deadline_at) + return this.terminalWithoutAllocation(build, 'failed', 'deadline'); + if (this.deps.store.environment()?.deletedAt != null) { + return this.terminalWithoutAllocation(build, 'obsolete', 'environment_deleted'); + } + const input = parseBuildInput(build); + const reference = await this.deps.environment.reference(this.environmentId); + if (reference === null) { + this.deps.store.markDeleted(now); + return this.terminalWithoutAllocation(build, 'obsolete', 'environment_deleted'); + } + if ( + reference.configurationRevision !== build.revision || + !sameEnvironmentScope(reference, input.reference) + ) { + return this.terminalWithoutAllocation(build, 'obsolete', 'configuration_changed'); + } + const access = await this.deps.environment.actorAccess(reference.owner, build.actor_user_id); + if (access === 'none' || (build.trigger === 'manual' && access !== 'manage')) { + return this.terminalWithoutAllocation(build, 'failed', 'actor_unauthorized'); + } + const sandboxId = this.deps.builderSandboxId; + if ( + (await this.deps.environment.capacity(reference.owner, sandboxId, build.actor_user_id)) === + 'limit_reached' + ) { + return this.terminalWithoutAllocation(build, 'failed', 'container_limit_reached'); + } + const git = await this.deps.environment.gitSource({ + reference: input.reference, + repoFullName: input.repoFullName, + actorUserId: build.actor_user_id, + }); + if (!git.ok) { + if (git.reason === 'unavailable') return; // The next alarm retries within the deadline. + return this.terminalWithoutAllocation(build, 'failed', 'repository_unauthorized'); + } + const allocation: BuilderAllocation = { + sandboxId, + allocationId: this.deps.uuid(), + instance: build.instance as BuilderAllocation['instance'], + }; + // Plan step 2: the allocation intent is durable before the provider request. + const preparing = this.deps.store.transition(build.id, ['queued'], { + state: 'preparing', + allocation_id: allocation.allocationId, + stage_times: withStage(build, 'preparing', this.deps.now()), + }); + if (preparing === null) return; + await this.launch(preparing); + } + + private async launch(build: BuildRow): Promise { + const allocation = this.allocationOf(build); + const url = this.deps.runnerUrl(this.environmentId, build.id); + if (url === null) { + this.beginStopping(build.id, ['preparing'], 'failed', { reason: 'builder_start_failed' }); + return; + } + const input = parseBuildInput(build); + try { + await this.deps.provider.admitBilling( + allocation, + this.deps.billingInput({ + owner: input.reference.owner, + actorUserId: build.actor_user_id, + sandboxId: allocation.sandboxId, + buildId: build.id, + }) + ); + } catch (error) { + const reason = + error instanceof ProviderCreationError && error.code === 'insufficient_credits' + ? 'billing_blocked' + : 'billing_unavailable'; + this.beginStopping(build.id, ['preparing'], 'failed', { reason }); + return; + } + if (this.deps.store.build(build.id)?.state !== 'preparing') return; + const credential = await this.deps.runnerCredential({ + environmentId: this.environmentId, + buildId: build.id, + allocationId: allocation.allocationId, + }); + try { + await this.deps.provider.launch(allocation, { + runner: { url, credential }, + deadlineAt: build.deadline_at, + }); + } catch { + const reason = 'builder_start_failed' as const; + this.deps.log('Environment builder start failed', { + environmentId: this.environmentId, + buildId: build.id, + reason, + }); + this.beginStopping(build.id, ['preparing'], 'failed', { reason }); + return; + } + const now = this.deps.now(); + this.lastLeaseRenewalAt = now; + this.deps.store.transition(build.id, ['preparing'], { + launched_at: now, + stage_times: withStage(build, 'launched', now), + }); + } + + private async advancePreparing(build: BuildRow): Promise { + const now = this.deps.now(); + if (now >= build.deadline_at) { + this.beginStopping(build.id, ['preparing'], 'failed', { reason: 'deadline' }); + return; + } + if (this.deps.store.environment()?.deletedAt != null) { + this.beginStopping(build.id, ['preparing'], 'obsolete', { reason: 'environment_deleted' }); + return; + } + if (build.launched_at === null) { + // A restart interrupted the start; the builder launch is idempotent per allocation. + await this.launch(build); + return; + } + const reference = await this.deps.environment.reference(this.environmentId); + if (reference === null) { + this.deps.store.markDeleted(now); + this.beginStopping(build.id, ['preparing'], 'obsolete', { reason: 'environment_deleted' }); + return; + } + if (reference.configurationRevision !== build.revision) { + this.beginStopping(build.id, ['preparing'], 'obsolete', { reason: 'configuration_changed' }); + return; + } + const lastReport = build.last_heartbeat_at; + if ( + (lastReport === null && now - build.launched_at >= BUILDER_FIRST_REPORT_MS) || + (lastReport !== null && now - lastReport >= BUILDER_REPORT_LOSS_MS) + ) { + this.beginStopping(build.id, ['preparing'], 'failed', { reason: 'builder_lost' }); + return; + } + if (now - this.lastLeaseRenewalAt >= LEASE_RENEW_INTERVAL_MS) { + this.lastLeaseRenewalAt = now; + await this.deps.provider.renewLease(this.allocationOf(build)).catch(() => undefined); + } + } + + /** Plan step 8: capture under the persisted id; a lost reply is read back, never retaken. */ + private async advanceCapturing(build: BuildRow): Promise { + if (this.deps.now() >= build.deadline_at) { + this.beginStopping(build.id, ['capturing'], 'failed', { reason: 'deadline' }); + return; + } + if (build.capture_id === null) { + this.beginStopping(build.id, ['capturing'], 'failed', { reason: 'capture_failed' }); + return; + } + await this.deps.provider.renewLease(this.allocationOf(build)).catch(() => undefined); + let result; + try { + result = await this.deps.provider.capture(this.allocationOf(build), build.capture_id); + } catch { + // The reply was lost; the next pass reads the stored result by capture id. + return; + } + if (result.status === 'in_progress') return; + if (result.status === 'failed') { + this.beginStopping(build.id, ['capturing'], 'failed', { + reason: result.reason === 'uncertain' ? 'capture_uncertain' : 'capture_failed', + }); + return; + } + const now = this.deps.now(); + const publishing = this.deps.store.transition(build.id, ['capturing'], { + state: 'publishing', + capture_result: JSON.stringify(result), + stage_times: withStage(build, 'publishing', now), + }); + if (publishing !== null) await this.advancePublishing(publishing); + } + + /** Plan steps 9 and 10: recheck everything, then replace the descriptor atomically. */ + private async advancePublishing(build: BuildRow): Promise { + const now = this.deps.now(); + if (this.deps.store.environment()?.deletedAt != null) { + this.beginStopping(build.id, ['publishing'], 'obsolete', { reason: 'environment_deleted' }); + return; + } + const input = parseBuildInput(build); + const reference = await this.deps.environment.reference(this.environmentId); + if (reference === null) { + this.deps.store.markDeleted(now); + this.beginStopping(build.id, ['publishing'], 'obsolete', { reason: 'environment_deleted' }); + return; + } + if ( + reference.configurationRevision !== build.revision || + !sameEnvironmentScope(reference, input.reference) + ) { + this.beginStopping(build.id, ['publishing'], 'obsolete', { reason: 'configuration_changed' }); + return; + } + const access = await this.deps.environment.actorAccess(reference.owner, build.actor_user_id); + if (access === 'none' || (build.trigger === 'manual' && access !== 'manage')) { + this.beginStopping(build.id, ['publishing'], 'failed', { reason: 'actor_unauthorized' }); + return; + } + const git = await this.deps.environment.gitSource({ + reference: input.reference, + repoFullName: input.repoFullName, + actorUserId: build.actor_user_id, + }); + if (!git.ok) { + if (git.reason === 'unavailable' && now < build.deadline_at) return; + this.beginStopping(build.id, ['publishing'], 'failed', { reason: 'repository_unauthorized' }); + return; + } + const capture = parseCaptureResult(build.capture_result); + if (capture === null || build.source_commit === null || build.source_branch === null) { + this.beginStopping(build.id, ['publishing'], 'failed', { reason: 'capture_failed' }); + return; + } + const descriptor: PublishedSnapshot = { + buildId: build.id, + environmentId: this.environmentId, + owner: input.reference.owner, + revision: build.revision, + repository: input.reference.repository, + sourceCommit: build.source_commit, + sourceBranch: build.source_branch, + provider: 'cloudflare-containers', + baseImage: capture.baseImage, + runtimeAbi: RUNTIME_BASE_ABI, + runtimeRelease: build.runtime_release, + builderInstance: build.instance, + snapshotId: capture.snapshotId, + ...(capture.size === undefined ? {} : { size: capture.size }), + capturedAt: capture.capturedAt, + publishedAt: now, + setupDurationMs: build.setup_duration_ms ?? 0, + }; + const published = this.deps.transaction(() => { + const stopping = this.beginStopping(build.id, ['publishing'], 'succeeded', {}); + if (stopping === null) return false; + this.deps.store.publish(descriptor); + return true; + }); + if (published) { + this.deps.log('Environment snapshot published', { + environmentId: this.environmentId, + buildId: build.id, + revision: build.revision, + setupDurationMs: build.setup_duration_ms ?? 0, + }); + } + } + + /** Plan step 11: stop the exact builder allocation; retain evidence until confirmed. */ + private async advanceStopping(build: BuildRow): Promise { + const now = this.deps.now(); + if (build.next_cleanup_at !== null && build.next_cleanup_at > now) return; + if (build.allocation_id === null) { + this.finalize(build); + return; + } + let result: 'terminal' | 'retryable'; + try { + result = await this.deps.provider.stop(this.allocationOf(build)); + } catch { + result = 'retryable'; + } + if (result === 'terminal') { + this.finalize(build); + return; + } + const attempts = build.cleanup_attempts + 1; + this.deps.store.transition(build.id, ['stopping'], { + cleanup_attempts: attempts, + next_cleanup_at: now + cleanupRetryDelay(attempts), + cleanup_error: attempts >= CLEANUP_ATTEMPTS_BEFORE_FAILURE ? 'stop_unconfirmed' : null, + }); + } + + private finalize(build: BuildRow): void { + const outcome = build.outcome ?? 'failed'; + const done = this.deps.store.transition(build.id, ['stopping'], { + state: outcome, + encrypted_secrets: null, + cleanup_error: null, + next_cleanup_at: null, + finished_at: this.deps.now(), + stage_times: withStage(build, 'finished', this.deps.now()), + }); + if (done === null) return; + this.recordAutomaticOutcome(done); + this.deps.log('Environment build finished', { + environmentId: this.environmentId, + buildId: build.id, + outcome, + reason: build.failure_reason, + }); + } + + private recordAutomaticOutcome(build: BuildRow): void { + if (build.state === 'succeeded') { + this.deps.store.recordAutoSuccess(build.revision); + return; + } + if ( + build.trigger === 'session' && + build.state === 'failed' && + build.failure_reason !== null && + COUNTED_FAILURE_REASONS.has(build.failure_reason) + ) { + this.deps.store.recordAutoFailure(build.revision, this.deps.now()); + } + } + + /** + * Persist cleanup intent with the intended outcome (plan: "Persist cleanup + * intent before stopping"). A failure reason is written only with a failed + * or obsolete outcome. + */ + private beginStopping( + buildId: string, + from: readonly BuildState[], + outcome: BuildOutcome, + failure: { reason?: string; detail?: string } + ): BuildRow | null { + const now = this.deps.now(); + const current = this.deps.store.build(buildId); + if (current === null) return null; + return this.deps.store.transition(buildId, from, { + state: 'stopping', + outcome, + ...(failure.reason === undefined ? {} : { failure_reason: failure.reason }), + ...(failure.detail === undefined ? {} : { failure_detail: failure.detail }), + next_cleanup_at: null, + stage_times: withStage(current, 'stopping', now), + }); + } + + private allocationOf(build: BuildRow): BuilderAllocation { + if (build.allocation_id === null) throw new Error('Build has no allocation'); + return { + sandboxId: this.deps.builderSandboxId, + allocationId: build.allocation_id, + instance: build.instance as BuilderAllocation['instance'], + }; + } + + private async failRunning(buildId: string, reason: string, detail?: string): Promise { + const stopping = this.beginStopping(buildId, ['preparing'], 'failed', { + reason, + ...(detail === undefined ? {} : { detail }), + }); + if (stopping !== null) await this.deps.armAlarm(this.deps.now()); + } +} + +type StoredCapture = { + snapshotId: string; + size?: number; + capturedAt: number; + baseImage: string; +}; + +function parseCaptureResult(value: string | null): StoredCapture | null { + if (value === null) return null; + try { + const parsed = JSON.parse(value) as Partial & { status?: string }; + if ( + parsed.status !== 'stored' || + typeof parsed.snapshotId !== 'string' || + typeof parsed.capturedAt !== 'number' || + typeof parsed.baseImage !== 'string' + ) { + return null; + } + return { + snapshotId: parsed.snapshotId, + capturedAt: parsed.capturedAt, + baseImage: parsed.baseImage, + ...(typeof parsed.size === 'number' ? { size: parsed.size } : {}), + }; + } catch { + return null; + } +} + +function summarize(build: BuildRow): EnvironmentBuildSummary { + const cleanup = + build.allocation_id === null + ? 'not_started' + : build.state === 'stopping' + ? build.cleanup_error === null + ? 'pending' + : 'failing' + : isTerminal(build.state) + ? 'confirmed' + : 'not_started'; + const step = build.runner_step; + return { + buildId: build.id, + revision: build.revision, + trigger: build.trigger, + state: build.state, + outcome: build.outcome ?? null, + step: step === 'clone' || step === 'setup' || step === 'cleanup' ? step : null, + failureReason: build.failure_reason, + failureDetail: build.failure_detail, + sourceCommit: build.source_commit, + sourceBranch: build.source_branch, + setupDurationMs: build.setup_duration_ms, + createdAt: build.created_at, + finishedAt: build.finished_at, + cleanup, + logTruncated: build.log_truncated, + }; +} + +function summarizeSnapshot( + published: PublishedSnapshot, + currentRevision: number, + now: number, + current: SnapshotCompatibility +) { + const stale = !compatibleWith(published, current) + ? ('image' as const) + : published.revision < currentRevision + ? ('revision' as const) + : replacementNeeded(published, published.revision, now, current) + ? ('age' as const) + : null; + return { + buildId: published.buildId, + revision: published.revision, + sourceCommit: published.sourceCommit, + sourceBranch: published.sourceBranch, + capturedAt: published.capturedAt, + size: published.size ?? null, + setupDurationMs: published.setupDurationMs, + stale, + expiresAt: published.capturedAt + SNAPSHOT_ELIGIBLE_LIFETIME_MS, + }; +} diff --git a/services/cloud-agent-next/src/environment-build/drizzle/0000_environment_build.sql b/services/cloud-agent-next/src/environment-build/drizzle/0000_environment_build.sql new file mode 100644 index 00000000000..9e67034c668 --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/drizzle/0000_environment_build.sql @@ -0,0 +1,64 @@ +CREATE TABLE `auto_attempts` ( + `revision` integer PRIMARY KEY NOT NULL, + `failures` integer NOT NULL, + `last_failure_at` integer, + `last_admitted_at` integer +); +--> statement-breakpoint +CREATE TABLE `build_logs` ( + `build_id` text NOT NULL, + `seq` integer NOT NULL, + `text` text NOT NULL, + PRIMARY KEY(`build_id`, `seq`) +); +--> statement-breakpoint +CREATE TABLE `builds` ( + `id` text PRIMARY KEY NOT NULL, + `request_id` text NOT NULL, + `revision` integer NOT NULL, + `trigger` text NOT NULL, + `actor_user_id` text NOT NULL, + `input` text NOT NULL, + `encrypted_secrets` text, + `instance` text NOT NULL, + `runtime_release` text, + `state` text NOT NULL, + `outcome` text, + `failure_reason` text, + `failure_detail` text, + `allocation_id` text, + `credential_hash` text, + `launched_at` integer, + `last_heartbeat_at` integer, + `last_event_sequence` integer, + `runner_step` text, + `source_commit` text, + `source_branch` text, + `setup_duration_ms` integer, + `capture_id` text, + `capture_result` text, + `cleanup_attempts` integer NOT NULL, + `cleanup_error` text, + `next_cleanup_at` integer, + `log_seq` integer NOT NULL, + `log_bytes` integer NOT NULL, + `log_truncated` integer NOT NULL, + `stage_times` text NOT NULL, + `created_at` integer NOT NULL, + `deadline_at` integer NOT NULL, + `finished_at` integer +); +--> statement-breakpoint +CREATE UNIQUE INDEX `builds_request_id` ON `builds` (`request_id`);--> statement-breakpoint +CREATE INDEX `builds_state` ON `builds` (`state`);--> statement-breakpoint +CREATE INDEX `builds_created_at` ON `builds` (`created_at`);--> statement-breakpoint +CREATE TABLE `environment` ( + `id` text PRIMARY KEY NOT NULL, + `environment_id` text NOT NULL, + `deleted_at` integer +); +--> statement-breakpoint +CREATE TABLE `published_snapshot` ( + `id` text PRIMARY KEY NOT NULL, + `descriptor` text NOT NULL +); diff --git a/services/cloud-agent-next/src/environment-build/drizzle/meta/0000_snapshot.json b/services/cloud-agent-next/src/environment-build/drizzle/meta/0000_snapshot.json new file mode 100644 index 00000000000..81740ea72da --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/drizzle/meta/0000_snapshot.json @@ -0,0 +1,420 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "7738b24c-ca10-4350-b9c9-6692bb2b779a", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "auto_attempts": { + "name": "auto_attempts", + "columns": { + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "failures": { + "name": "failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_failure_at": { + "name": "last_failure_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_admitted_at": { + "name": "last_admitted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "build_logs": { + "name": "build_logs", + "columns": { + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "build_logs_build_id_seq_pk": { + "columns": [ + "build_id", + "seq" + ], + "name": "build_logs_build_id_seq_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "builds": { + "name": "builds", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input": { + "name": "input", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_secrets": { + "name": "encrypted_secrets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instance": { + "name": "instance", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "runtime_release": { + "name": "runtime_release", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failure_detail": { + "name": "failure_detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "allocation_id": { + "name": "allocation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential_hash": { + "name": "credential_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "launched_at": { + "name": "launched_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_event_sequence": { + "name": "last_event_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runner_step": { + "name": "runner_step", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_commit": { + "name": "source_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_branch": { + "name": "source_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "setup_duration_ms": { + "name": "setup_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "capture_id": { + "name": "capture_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "capture_result": { + "name": "capture_result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cleanup_attempts": { + "name": "cleanup_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanup_error": { + "name": "cleanup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_cleanup_at": { + "name": "next_cleanup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "log_seq": { + "name": "log_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "log_truncated": { + "name": "log_truncated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "stage_times": { + "name": "stage_times", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deadline_at": { + "name": "deadline_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "builds_request_id": { + "name": "builds_request_id", + "columns": [ + "request_id" + ], + "isUnique": true + }, + "builds_state": { + "name": "builds_state", + "columns": [ + "state" + ], + "isUnique": false + }, + "builds_created_at": { + "name": "builds_created_at", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment": { + "name": "environment", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "published_snapshot": { + "name": "published_snapshot", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "descriptor": { + "name": "descriptor", + "type": "text", + "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/src/environment-build/drizzle/meta/_journal.json b/services/cloud-agent-next/src/environment-build/drizzle/meta/_journal.json new file mode 100644 index 00000000000..0f9d78473ee --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1792104003857, + "tag": "0000_environment_build", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/services/cloud-agent-next/src/environment-build/drizzle/migrations.js b/services/cloud-agent-next/src/environment-build/drizzle/migrations.js new file mode 100644 index 00000000000..d77b67c67df --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/drizzle/migrations.js @@ -0,0 +1,9 @@ +import journal from './meta/_journal.json'; +import m0000 from './0000_environment_build.sql'; + +export default { + journal, + migrations: { + m0000, + }, +}; diff --git a/services/cloud-agent-next/src/environment-build/enrollment.test.ts b/services/cloud-agent-next/src/environment-build/enrollment.test.ts new file mode 100644 index 00000000000..54dc5056494 --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/enrollment.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; +import { isEnvironmentSnapshotsEnabled } from './enrollment.js'; + +describe('snapshot organization enrollment', () => { + const owner = { type: 'organization', id: 'org-a' } as const; + + it('defaults to disabled and does not accept a wildcard', () => { + for (const value of [undefined, '', ' ', '*', 'org-ab']) { + expect(isEnvironmentSnapshotsEnabled({ ENVIRONMENT_SNAPSHOT_ORG_IDS: value }, owner)).toBe( + false + ); + } + }); + + it('matches only listed organizations, with whitespace allowed', () => { + const env = { ENVIRONMENT_SNAPSHOT_ORG_IDS: ' org-b, org-a , ' }; + expect(isEnvironmentSnapshotsEnabled(env, owner)).toBe(true); + expect(isEnvironmentSnapshotsEnabled(env, { type: 'organization', id: 'org-c' })).toBe(false); + expect(isEnvironmentSnapshotsEnabled(env, { type: 'user', id: owner.id })).toBe(false); + }); +}); diff --git a/services/cloud-agent-next/src/environment-build/enrollment.ts b/services/cloud-agent-next/src/environment-build/enrollment.ts new file mode 100644 index 00000000000..3c07021e2d6 --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/enrollment.ts @@ -0,0 +1,14 @@ +import type { SessionEnvironmentReference } from './reference.js'; + +/** Snapshot access is separate from native-container enrollment. No wildcard or personal access. */ +export function isEnvironmentSnapshotsEnabled( + env: { ENVIRONMENT_SNAPSHOT_ORG_IDS?: string }, + owner: SessionEnvironmentReference['owner'] +): boolean { + return ( + owner.type === 'organization' && + (env.ENVIRONMENT_SNAPSHOT_ORG_IDS ?? '') + .split(',') + .some(id => id.trim() !== '*' && id.trim() === owner.id) + ); +} diff --git a/services/cloud-agent-next/src/environment-build/environment-build-do.ts b/services/cloud-agent-next/src/environment-build/environment-build-do.ts new file mode 100644 index 00000000000..7de2da39727 --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/environment-build-do.ts @@ -0,0 +1,283 @@ +import { DurableObject } from 'cloudflare:workers'; +import { isEnvironmentSnapshotsEnabled } from './enrollment.js'; +import { drizzle, type DrizzleSqliteDODatabase } from 'drizzle-orm/durable-sqlite'; +import { migrate } from 'drizzle-orm/durable-sqlite/migrator'; +import { timingSafeEqual } from '@kilocode/encryption'; +import { + loadEnvironmentBuildInput, + readEnvironmentReference, + type EnvironmentReference, +} from '@kilocode/cloud-agent-profile'; +import { resolveSecret } from '../auth.js'; +import { + assertContainerCapacity, + getContainerCapacityFailureReason, +} from '../container-concurrency.js'; +import { isCloudAgentContainerBillingEnabled } from '../container-billing-rollout.js'; +import { isCloudflareContainersEnrolled } from '../agent-sandbox/cloudflare-containers/cloudflare-containers-runtime-config.js'; +import { getPgDb } from '../db/pg.js'; +import { logger } from '../logger.js'; +import { mergeEnvVarsWithSecrets, type EncryptedSecrets } from '../utils/encryption.js'; +import type { Env } from '../types.js'; +import type { + EnvironmentBuildEvents, + EnvironmentBuildEventsReply, + EnvironmentBuildInput, + EnvironmentBuildResult, +} from '../shared/environment-build-protocol.js'; +import type { + EnvironmentBuildCancelResult, + EnvironmentBuildLogs, + EnvironmentBuildRequestResult, + EnvironmentBuildStatus, +} from '../shared/environment-build-status.js'; +import { environmentActorAccess, resolveBuilderGitSource } from './access.js'; +import { builderSandboxId, createBuilderProvider } from './builder-provider.js'; +import { + EnvironmentBuildCoordinator, + type CoordinatorDeps, + type RunnerAuth, + type RunnerRefusal, + type SnapshotResolution, +} from './coordinator.js'; +import { mintRunnerCredential } from './runner-credential.js'; +import type { SessionEnvironmentReference } from './reference.js'; +import type { BuildTrigger } from './sqlite-schema.js'; +import { BuildStore } from './store.js'; +import migrations from './drizzle/migrations'; + +/** + * One coordinator per environment id (`idFromName(environmentId)`). The DO is a + * thin host: it migrates its SQLite, builds the coordinator's dependencies from + * the Worker environment and forwards each RPC and the alarm. + */ + +/** The derived runner credential: an HMAC over the build and allocation, never stored. */ +async function deriveRunnerCredential( + secret: string, + claims: { environmentId: string; buildId: string; allocationId: string } +): Promise { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(secret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + const message = `environment-build-runner:v1:${claims.environmentId}:${claims.buildId}:${claims.allocationId}`; + const digest = new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(message))); + return Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join(''); +} + +function billingSubject(owner: EnvironmentReference['owner']) { + return owner.type === 'organization' + ? { type: 'org' as const, id: owner.id } + : { type: 'user' as const, id: owner.id }; +} + +export class EnvironmentBuild extends DurableObject { + private readonly db: DrizzleSqliteDODatabase; + private readonly store: BuildStore; + private readonly initialized: Promise; + private coordinator: EnvironmentBuildCoordinator | undefined; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.db = drizzle(ctx.storage, { logger: false }); + this.store = new BuildStore(this.db); + this.initialized = ctx.blockConcurrencyWhile(async () => { + await migrate(this.db, migrations); + }); + } + + private async coordinatorFor(environmentId: string): Promise { + await this.initialized; + if (this.coordinator !== undefined) return this.coordinator; + const bound = this.store.environment(); + if (bound !== null && bound.environmentId !== environmentId) { + throw new Error('Environment build coordinator serves another environment'); + } + const sandboxId = await builderSandboxId(environmentId); + this.coordinator = new EnvironmentBuildCoordinator(environmentId, this.createDeps(sandboxId)); + return this.coordinator; + } + + private createDeps(sandboxId: string): CoordinatorDeps { + const env = this.env; + const storage = this.ctx.storage; + const signingSecret = async (): Promise => { + const secret = await resolveSecret(env.NEXTAUTH_SECRET); + if (!secret) throw new Error('Environment build signing secret is unavailable'); + return secret; + }; + return { + now: () => Date.now(), + uuid: () => crypto.randomUUID(), + store: this.store, + transaction: write => storage.transactionSync(write), + armAlarm: async at => { + const current = await storage.getAlarm(); + if (current === null || at < current) await storage.setAlarm(at); + }, + environment: { + reference: id => readEnvironmentReference(getPgDb(env), id), + buildInput: id => loadEnvironmentBuildInput(getPgDb(env), id), + actorAccess: (owner, userId) => environmentActorAccess(getPgDb(env), owner, userId), + gitSource: input => resolveBuilderGitSource(env, input), + capacity: async (owner, instanceId, actorUserId) => { + try { + await assertContainerCapacity(env, { + subject: billingSubject(owner), + instanceId, + actor: { type: 'user', id: actorUserId }, + checkpoint: 'sandbox-start', + }); + return 'ok'; + } catch (error) { + if (getContainerCapacityFailureReason(error) === 'container_limit_reached') + return 'limit_reached'; + throw error; + } + }, + }, + provider: createBuilderProvider({ + containers: env.SANDBOX_CONTAINERS, + env, + }), + runnerUrl: (environmentId, buildId) => { + const base = env.WORKER_URL?.replace(/\/$/, ''); + return base ? `${base}/environment-build/${environmentId}/${buildId}` : null; + }, + runnerCredential: async claims => { + const secret = await signingSecret(); + return mintRunnerCredential( + { ...claims, credential: await deriveRunnerCredential(secret, claims) }, + secret + ); + }, + runnerCredentialMatches: async (claims, credential) => { + const secret = await signingSecret(); + return timingSafeEqual(await deriveRunnerCredential(secret, claims), credential); + }, + decryptSecrets: encrypted => + mergeEnvVarsWithSecrets({}, encrypted as EncryptedSecrets, env.AGENT_ENV_VARS_PRIVATE_KEY), + billingInput: ({ owner, actorUserId, sandboxId: billingSandboxId, buildId }) => { + const enforcementRequested = isCloudAgentContainerBillingEnabled(env, { + userId: actorUserId, + ...(owner.type === 'organization' ? { orgId: owner.id } : {}), + }); + return { + sandboxId: billingSandboxId as never, + sandboxKind: 'isolated', + subject: billingSubject(owner), + actor: { type: 'user', id: actorUserId }, + sessionId: `environment-build:${buildId}`, + metadata: { origin: 'environment-build' }, + ...(enforcementRequested ? { enforcementRequested: true } : {}), + }; + }, + builderSandboxId: sandboxId, + unsupportedReason: owner => { + if (!isEnvironmentSnapshotsEnabled(env, owner)) { + return 'Snapshots are not enabled for this organization'; + } + if (!env.SANDBOX_CONTAINERS) return 'Native containers are not configured'; + if ( + !isCloudflareContainersEnrolled( + env, + owner.type === 'organization' ? { orgId: owner.id } : {} + ) + ) { + return 'Cloudflare containers are not enabled for this account'; + } + return null; + }, + log: (message, fields) => logger.withFields(fields).info(message), + }; + } + + // --- public operations (authorized by the Worker handler) ------------------- + + async requestBuild(input: { + environmentId: string; + owner: EnvironmentReference['owner']; + expectedRevision: number; + requestId: string; + trigger: BuildTrigger; + actorUserId: string; + }): Promise { + const { environmentId, ...request } = input; + return (await this.coordinatorFor(environmentId)).requestBuild(request); + } + + async status(input: { + environmentId: string; + owner: EnvironmentReference['owner']; + configurationRevision: number; + }): Promise { + return await ( + await this.coordinatorFor(input.environmentId) + ).status(input.configurationRevision, input.owner); + } + + async logs(input: { + environmentId: string; + buildId: string; + cursor?: number; + limit: number; + }): Promise { + return (await this.coordinatorFor(input.environmentId)).logs( + input.buildId, + input.cursor, + input.limit + ); + } + + async cancel(input: { + environmentId: string; + buildId: string; + }): Promise { + return (await this.coordinatorFor(input.environmentId)).cancel(input.buildId); + } + + async environmentDeleted(input: { environmentId: string }): Promise { + await (await this.coordinatorFor(input.environmentId)).environmentDeleted(); + } + + // --- session launch (Sandbox DO) -------------------------------------------- + + async resolveSnapshot(input: { + reference: SessionEnvironmentReference; + actorUserId: string; + }): Promise { + return (await this.coordinatorFor(input.reference.environmentId)).resolveSnapshot(input); + } + + // --- builder callbacks (authenticated Worker routes) ------------------------ + + async runnerInput(auth: RunnerAuth): Promise { + return (await this.coordinatorFor(auth.environmentId)).runnerInput(auth); + } + + async runnerEvents( + auth: RunnerAuth, + events: EnvironmentBuildEvents + ): Promise { + return (await this.coordinatorFor(auth.environmentId)).runnerEvents(auth, events); + } + + async runnerResult( + auth: RunnerAuth, + result: EnvironmentBuildResult + ): Promise<'ok' | RunnerRefusal> { + return (await this.coordinatorFor(auth.environmentId)).runnerResult(auth, result); + } + + async alarm(): Promise { + await this.initialized; + const bound = this.store.environment(); + if (bound === null) return; + await (await this.coordinatorFor(bound.environmentId)).alarm(); + } +} diff --git a/services/cloud-agent-next/src/environment-build/policy.test.ts b/services/cloud-agent-next/src/environment-build/policy.test.ts new file mode 100644 index 00000000000..96dbc660b81 --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/policy.test.ts @@ -0,0 +1,195 @@ +import { describe, expect, it } from 'vitest'; +import { + SNAPSHOT_ELIGIBLE_LIFETIME_MS, + SNAPSHOT_STALE_AFTER_MS, + automaticAdmission, + cleanupRetryDelay, + replacementNeeded, + snapshotEligibility, + type PublishedSnapshot, +} from './policy.js'; +import type { SessionEnvironmentReference } from './reference.js'; + +const NOW = 1_800_000_000_000; +const CURRENT = { + baseImage: 'registry.example/app:1', + runtimeAbi: 'abi', + runtimeMode: 'preinstalled' as const, +}; +const HOUR = 60 * 60_000; + +const REFERENCE: SessionEnvironmentReference = { + environmentId: '11111111-1111-4111-8111-111111111111', + owner: { type: 'user', id: 'user-1' }, + configurationRevision: 2, + repository: { platform: 'github', repoFullName: 'acme/widgets' }, +}; + +function published(overrides: Partial = {}): PublishedSnapshot { + return { + buildId: '22222222-2222-4222-8222-222222222222', + environmentId: REFERENCE.environmentId, + owner: REFERENCE.owner, + revision: 2, + repository: REFERENCE.repository, + sourceCommit: 'a'.repeat(40), + sourceBranch: 'main', + provider: 'cloudflare-containers', + baseImage: 'registry.example/app:1', + runtimeAbi: 'abi', + runtimeRelease: null, + builderInstance: 'standard-4', + snapshotId: 'snap-1', + capturedAt: NOW - HOUR, + publishedAt: NOW - HOUR, + setupDurationMs: 1_000, + ...overrides, + }; +} + +describe('snapshotEligibility', () => { + it('does not let preinstalled startup execute aliases from a downloaded snapshot', () => { + const snapshot = published({ runtimeRelease: 'previous-release' }); + expect(snapshotEligibility(snapshot, REFERENCE, NOW, CURRENT)).toEqual({ + eligible: false, + reason: 'incompatible', + }); + expect( + snapshotEligibility(snapshot, REFERENCE, NOW, { ...CURRENT, runtimeMode: 'downloaded' }) + ).toEqual({ eligible: true, stale: false }); + }); + + it('uses a current snapshot without a replacement', () => { + expect(snapshotEligibility(published(), REFERENCE, NOW, CURRENT)).toEqual({ + eligible: true, + stale: false, + }); + expect(replacementNeeded(published(), 2, NOW, CURRENT)).toBe(false); + }); + + it('keeps an older revision usable while marking it stale', () => { + expect(snapshotEligibility(published({ revision: 1 }), REFERENCE, NOW, CURRENT)).toEqual({ + eligible: true, + stale: true, + staleReason: 'revision', + }); + expect(replacementNeeded(published({ revision: 1 }), 2, NOW, CURRENT)).toBe(true); + }); + + it('keeps an age-stale snapshot usable and asks for a refresh', () => { + const old = published({ capturedAt: NOW - SNAPSHOT_STALE_AFTER_MS }); + expect(snapshotEligibility(old, REFERENCE, NOW, CURRENT)).toEqual({ + eligible: true, + stale: true, + staleReason: 'age', + }); + expect(replacementNeeded(old, 2, NOW, CURRENT)).toBe(true); + }); + + it('never gives an older session a newer revision', () => { + expect(snapshotEligibility(published({ revision: 3 }), REFERENCE, NOW, CURRENT)).toEqual({ + eligible: false, + reason: 'newer_than_session', + }); + }); + + it('rejects another owner, repository or environment, and an expired snapshot', () => { + expect( + snapshotEligibility( + published({ owner: { type: 'user', id: 'other' } }), + REFERENCE, + NOW, + CURRENT + ) + ).toEqual({ eligible: false, reason: 'scope_mismatch' }); + expect( + snapshotEligibility( + published({ repository: { platform: 'github', repoFullName: 'acme/other' } }), + REFERENCE, + NOW, + CURRENT + ) + ).toEqual({ eligible: false, reason: 'scope_mismatch' }); + expect( + snapshotEligibility( + published({ environmentId: '33333333-3333-4333-8333-333333333333' }), + REFERENCE, + NOW, + CURRENT + ) + ).toEqual({ eligible: false, reason: 'scope_mismatch' }); + expect( + snapshotEligibility( + published({ capturedAt: NOW - SNAPSHOT_ELIGIBLE_LIFETIME_MS }), + REFERENCE, + NOW, + CURRENT + ) + ).toEqual({ eligible: false, reason: 'expired' }); + expect(snapshotEligibility(null, REFERENCE, NOW, CURRENT)).toEqual({ + eligible: false, + reason: 'missing', + }); + }); + + it('treats another base image or runtime ABI as incompatible and asks for a replacement', () => { + const otherImage = { ...CURRENT, baseImage: 'registry.example/app:2' }; + expect(snapshotEligibility(published(), REFERENCE, NOW, otherImage)).toEqual({ + eligible: false, + reason: 'incompatible', + }); + expect(replacementNeeded(published(), 2, NOW, otherImage)).toBe(true); + expect( + snapshotEligibility(published(), REFERENCE, NOW, { ...CURRENT, runtimeAbi: 'abi-2' }) + ).toEqual({ eligible: false, reason: 'incompatible' }); + }); + + it('leaves the image check to the container when the current image is unknown', () => { + const unknown = { ...CURRENT, baseImage: null }; + expect(snapshotEligibility(published(), REFERENCE, NOW, unknown).eligible).toBe(true); + expect(replacementNeeded(published(), 2, NOW, unknown)).toBe(false); + }); + + it('compares a GitHub repository case-insensitively', () => { + expect( + snapshotEligibility( + published({ repository: { platform: 'github', repoFullName: 'Acme/Widgets' } }), + REFERENCE, + NOW, + CURRENT + ).eligible + ).toBe(true); + }); +}); + +describe('automaticAdmission', () => { + it('admits the first attempt and after the backoff steps', () => { + expect(automaticAdmission(null, NOW)).toEqual({ admit: true }); + const once = { failures: 1, lastFailureAt: NOW, lastAdmittedAt: NOW }; + expect(automaticAdmission(once, NOW + 9 * 60_000)).toEqual({ + admit: false, + retryAt: NOW + 10 * 60_000, + }); + expect(automaticAdmission(once, NOW + 10 * 60_000)).toEqual({ admit: true }); + const twice = { failures: 2, lastFailureAt: NOW, lastAdmittedAt: NOW }; + expect(automaticAdmission(twice, NOW + 59 * 60_000).admit).toBe(false); + expect(automaticAdmission(twice, NOW + HOUR)).toEqual({ admit: true }); + }); + + it('admits at most one automatic build per day after three failures', () => { + const many = { failures: 3, lastFailureAt: NOW, lastAdmittedAt: NOW - HOUR }; + expect(automaticAdmission(many, NOW + 23 * HOUR)).toEqual({ + admit: false, + retryAt: NOW + 24 * HOUR, + }); + expect(automaticAdmission(many, NOW + 24 * HOUR)).toEqual({ admit: true }); + }); +}); + +describe('cleanupRetryDelay', () => { + it('backs off to a bounded interval', () => { + expect(cleanupRetryDelay(1)).toBe(5_000); + expect(cleanupRetryDelay(2)).toBe(10_000); + expect(cleanupRetryDelay(20)).toBe(5 * 60_000); + }); +}); diff --git a/services/cloud-agent-next/src/environment-build/policy.ts b/services/cloud-agent-next/src/environment-build/policy.ts new file mode 100644 index 00000000000..870b9dde65c --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/policy.ts @@ -0,0 +1,191 @@ +import { z } from 'zod'; +import { + environmentOwnerSchema, + environmentRepositoryIdentitySchema, + sameEnvironmentScope, + type SessionEnvironmentReference, +} from './reference.js'; + +/** + * Build and snapshot policy (plan "Ownership and data", "Build admission and + * execution", "Session launch"). Pure decisions over persisted records, so the + * coordinator, the status view and tests share one owner for each rule. + */ + +/** Absolute bound of one build, including allocation and capture. */ +export const BUILD_DEADLINE_MS = 30 * 60_000; +/** A published snapshot older than this is stale and admits a replacement. */ +export const SNAPSHOT_STALE_AFTER_MS = 4 * 24 * 60 * 60_000; +/** + * Cloudflare documents a 30-day snapshot TTL, refreshed on restore. A + * descriptor is ineligible a day earlier, so a start never races the expiry. + */ +export const SNAPSHOT_PLATFORM_LIFETIME_MS = 30 * 24 * 60 * 60_000; +export const SNAPSHOT_ELIGIBLE_LIFETIME_MS = SNAPSHOT_PLATFORM_LIFETIME_MS - 24 * 60 * 60_000; +/** A preparing builder with no report for this long is lost. */ +export const BUILDER_REPORT_LOSS_MS = 90_000; +/** A started builder has this long to make its first report. */ +export const BUILDER_FIRST_REPORT_MS = 5 * 60_000; +/** Terminal build records retained; the published descriptor is kept separately. */ +export const MAX_TERMINAL_BUILDS = 20; +export const BUILD_HISTORY_RETENTION_MS = 7 * 24 * 60 * 60_000; +/** Redacted log bytes kept per build. */ +export const MAX_BUILD_LOG_BYTES = 1024 * 1024; +/** Session snapshot lookup budget, including retries. */ +export const SNAPSHOT_LOOKUP_BUDGET_MS = 15_000; +/** One fresh session preparation attempt (`routePreparationMs`). */ +export const SESSION_PREPARATION_BUDGET_MS = 12 * 60_000; +/** Alarm cadence while a builder runs. */ +export const PREPARING_TICK_MS = 30_000; +/** Stop retries back off up to this interval and continue until confirmed. */ +export const CLEANUP_RETRY_MAX_MS = 5 * 60_000; +export const CLEANUP_ATTEMPTS_BEFORE_FAILURE = 5; + +export const publishedSnapshotSchema = z + .object({ + buildId: z.string().uuid(), + environmentId: z.string().uuid(), + owner: environmentOwnerSchema, + revision: z.number().int().positive(), + repository: environmentRepositoryIdentitySchema, + sourceCommit: z.string().regex(/^[0-9a-f]{40}$/), + sourceBranch: z.string().min(1).max(256), + provider: z.literal('cloudflare-containers'), + baseImage: z.string().min(1), + runtimeAbi: z.string().min(1), + runtimeRelease: z.string().min(1).nullable(), + builderInstance: z.string().min(1), + snapshotId: z.string().min(1), + size: z.number().nonnegative().optional(), + capturedAt: z.number().int().nonnegative(), + publishedAt: z.number().int().nonnegative(), + setupDurationMs: z.number().int().nonnegative(), + }) + .strict(); +export type PublishedSnapshot = z.infer; + +/** + * The launch image and runtime ABI a session would start with now. `baseImage` + * is null when it could not be read; the container DO still refuses another + * image, so an unknown image never blocks a start. + */ +export type SnapshotCompatibility = { + baseImage: string | null; + runtimeAbi: string; + runtimeMode: 'preinstalled' | 'downloaded'; +}; + +/** Whether `published` can start under `current` (plan "Session launch"). */ +export function compatibleWith( + published: PublishedSnapshot, + current: SnapshotCompatibility +): boolean { + return ( + published.runtimeAbi === current.runtimeAbi && + (current.runtimeMode === 'downloaded' || published.runtimeRelease === null) && + (current.baseImage === null || published.baseImage === current.baseImage) + ); +} + +/** Why a snapshot is not used for a session, or `eligible`. */ +export type SnapshotEligibility = + | { eligible: true; stale: false } + | { eligible: true; stale: true; staleReason: 'revision' | 'age' } + | { + eligible: false; + reason: 'missing' | 'scope_mismatch' | 'incompatible' | 'newer_than_session' | 'expired'; + }; + +/** + * Whether a session admitted at `reference` may start from `published`. An + * older revision or an age-stale snapshot stays usable while its replacement + * builds; a newer one must not introduce later setup into an older session. + * A snapshot for another base image or runtime ABI is incompatible. Instance + * size and runtime release are not part of compatibility. + */ +export function snapshotEligibility( + published: PublishedSnapshot | null, + reference: SessionEnvironmentReference, + now: number, + current: SnapshotCompatibility +): SnapshotEligibility { + if (published === null) return { eligible: false, reason: 'missing' }; + if (!sameEnvironmentScope(published, reference)) { + return { eligible: false, reason: 'scope_mismatch' }; + } + if (!compatibleWith(published, current)) return { eligible: false, reason: 'incompatible' }; + if (published.revision > reference.configurationRevision) { + return { eligible: false, reason: 'newer_than_session' }; + } + if (now - published.capturedAt >= SNAPSHOT_ELIGIBLE_LIFETIME_MS) { + return { eligible: false, reason: 'expired' }; + } + if (published.revision < reference.configurationRevision) { + return { eligible: true, stale: true, staleReason: 'revision' }; + } + if (now - published.capturedAt >= SNAPSHOT_STALE_AFTER_MS) { + return { eligible: true, stale: true, staleReason: 'age' }; + } + return { eligible: true, stale: false }; +} + +/** Whether `revision` needs a replacement build: no usable or no current snapshot. */ +export function replacementNeeded( + published: PublishedSnapshot | null, + revision: number, + now: number, + current: SnapshotCompatibility +): boolean { + if (published === null || published.revision !== revision) return true; + if (!compatibleWith(published, current)) return true; + return now - published.capturedAt >= SNAPSHOT_STALE_AFTER_MS; +} + +export type AutoAttempt = { + failures: number; + lastFailureAt: number | null; + lastAdmittedAt: number | null; +}; + +const BACKOFF_AFTER_FAILURES_MS = [0, 10 * 60_000, 60 * 60_000] as const; +const LONG_BACKOFF_MS = 24 * 60 * 60_000; + +/** + * Automatic admission backoff for one revision (proposed values): the first + * attempt is free; after one failure a later session may admit after 10 + * minutes, after two after 1 hour, and after three or more at most one + * automatic build per 24 hours. A manual Build is never subject to this. + */ +export function automaticAdmission( + attempt: AutoAttempt | null, + now: number +): { admit: true } | { admit: false; retryAt: number } { + if (attempt === null || attempt.failures === 0) return { admit: true }; + if (attempt.failures < BACKOFF_AFTER_FAILURES_MS.length) { + const retryAt = + (attempt.lastFailureAt ?? 0) + (BACKOFF_AFTER_FAILURES_MS[attempt.failures] ?? 0); + return now >= retryAt ? { admit: true } : { admit: false, retryAt }; + } + const retryAt = + Math.max(attempt.lastFailureAt ?? 0, attempt.lastAdmittedAt ?? 0) + LONG_BACKOFF_MS; + return now >= retryAt ? { admit: true } : { admit: false, retryAt }; +} + +/** Builder failure reasons that count toward automatic backoff; admission refusals do not. */ +export const COUNTED_FAILURE_REASONS: ReadonlySet = new Set([ + 'clone_failed', + 'setup_failed', + 'setup_timeout', + 'cleanup_failed', + 'capture_failed', + 'capture_uncertain', + 'deadline', + 'builder_lost', + 'builder_start_failed', + 'input_unavailable', +]); + +/** Stop retry delay after `attempts` unconfirmed stops. */ +export function cleanupRetryDelay(attempts: number): number { + return Math.min(CLEANUP_RETRY_MAX_MS, 5_000 * 2 ** Math.max(0, attempts - 1)); +} diff --git a/services/cloud-agent-next/src/environment-build/reference.ts b/services/cloud-agent-next/src/environment-build/reference.ts new file mode 100644 index 00000000000..be0ac7d48d4 --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/reference.ts @@ -0,0 +1,85 @@ +import { z } from 'zod'; +import type { + EnvironmentReference, + EnvironmentRepositoryIdentity, +} from '@kilocode/cloud-agent-profile'; + +/** + * The authorized environment a session admitted (plan "Session launch"). Session + * preflight produces it after it authorized the environment and the repository; + * it rides session metadata, the Session DO registration and the Sandbox DO + * route as DO-private data, like the credential source. It never reaches a + * wrapper frame. + */ +export const environmentRepositoryIdentitySchema = z.discriminatedUnion('platform', [ + z.object({ platform: z.literal('github'), repoFullName: z.string().min(1).max(500) }).strict(), + z.object({ platform: z.literal('gitlab'), cloneUrl: z.string().min(1).max(2048) }).strict(), + z + .object({ + platform: z.literal('bitbucket'), + workspaceUuid: z.string().min(1).max(64), + repositoryUuid: z.string().min(1).max(64), + }) + .strict(), +]); + +export const environmentOwnerSchema = z.discriminatedUnion('type', [ + z.object({ type: z.literal('user'), id: z.string().min(1).max(256) }).strict(), + z.object({ type: z.literal('organization'), id: z.string().uuid() }).strict(), +]); + +export const environmentReferenceSchema = z + .object({ + environmentId: z.string().uuid(), + owner: environmentOwnerSchema, + configurationRevision: z.number().int().positive(), + repository: environmentRepositoryIdentitySchema, + }) + .strict(); + +export type SessionEnvironmentReference = z.infer; + +// The profile package owns the shape; this schema must accept every value it produces. +type AssertAssignable = T; +export type _ReferenceMatchesProfilePackage = AssertAssignable< + EnvironmentReference, + SessionEnvironmentReference +>; +export type _RepositoryMatchesProfilePackage = AssertAssignable< + EnvironmentRepositoryIdentity, + z.infer +>; + +function sameRepository( + left: EnvironmentRepositoryIdentity, + right: EnvironmentRepositoryIdentity +): boolean { + switch (left.platform) { + case 'github': + return ( + right.platform === 'github' && + left.repoFullName.toLowerCase() === right.repoFullName.toLowerCase() + ); + case 'gitlab': + return right.platform === 'gitlab' && left.cloneUrl === right.cloneUrl; + case 'bitbucket': + return ( + right.platform === 'bitbucket' && + left.workspaceUuid.toLowerCase() === right.workspaceUuid.toLowerCase() && + left.repositoryUuid.toLowerCase() === right.repositoryUuid.toLowerCase() + ); + } +} + +/** Same environment, owner and repository; revisions are compared by the caller. */ +export function sameEnvironmentScope( + left: Pick, + right: Pick +): boolean { + return ( + left.environmentId === right.environmentId && + left.owner.type === right.owner.type && + left.owner.id === right.owner.id && + sameRepository(left.repository, right.repository) + ); +} diff --git a/services/cloud-agent-next/src/environment-build/routes.ts b/services/cloud-agent-next/src/environment-build/routes.ts new file mode 100644 index 00000000000..21769018fe9 --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/routes.ts @@ -0,0 +1,104 @@ +import type { Context, Hono } from 'hono'; +import { z } from 'zod'; +import { resolveSecret } from '../auth.js'; +import type { HonoContext } from '../hono-context.js'; +import { + environmentBuildEventsSchema, + environmentBuildResultSchema, +} from '../shared/environment-build-protocol.js'; +import type { RunnerAuth, RunnerRefusal } from './coordinator.js'; +import { parseRunnerBearer, verifyRunnerCredential } from './runner-credential.js'; + +/** + * The build-specific control endpoint (plan: "The runner uses a build-specific + * authenticated control endpoint"). Each request carries the signed runner + * credential; the path must name the same environment and build, and the + * coordinator checks the allocation and derived credential before acting. A + * session credential can never pass here, and this route never reaches a + * Sandbox DO. + */ + +const MAX_BODY_BYTES = 256 * 1024; +const idSchema = z.string().uuid(); + +type RunnerContext = Context; + +function refusalResponse(c: RunnerContext, refusal: RunnerRefusal): Response { + switch (refusal.refused) { + case 'unauthorized': + return c.json({ error: 'unauthorized' }, 401); + case 'gone': + return c.json({ error: 'build is not running' }, 410); + case 'unavailable': + return c.json({ error: 'temporarily unavailable' }, 503); + } +} + +async function authenticate(c: RunnerContext): Promise { + const environmentId = idSchema.safeParse(c.req.param('environmentId')); + const buildId = idSchema.safeParse(c.req.param('buildId')); + if (!environmentId.success || !buildId.success) return c.json({ error: 'not found' }, 404); + const token = parseRunnerBearer(c.req.header('authorization') ?? null); + if (token === null) return c.json({ error: 'unauthorized' }, 401); + const secret = await resolveSecret(c.env.NEXTAUTH_SECRET).catch(() => null); + if (!secret) return c.json({ error: 'temporarily unavailable' }, 503); + const claims = verifyRunnerCredential(token, secret); + if (claims === null) return c.json({ error: 'unauthorized' }, 401); + if (claims.environmentId !== environmentId.data || claims.buildId !== buildId.data) { + return c.json({ error: 'unauthorized' }, 401); + } + return { + environmentId: claims.environmentId, + buildId: claims.buildId, + allocationId: claims.allocationId, + credential: claims.credential, + }; +} + +async function readJson(c: RunnerContext): Promise { + const text = await c.req.text(); + if (new TextEncoder().encode(text).byteLength > MAX_BODY_BYTES) throw new Error('body too large'); + return JSON.parse(text) as unknown; +} + +function coordinator(c: RunnerContext, environmentId: string) { + return c.env.ENVIRONMENT_BUILD.getByName(environmentId); +} + +export function registerEnvironmentBuildRoutes(app: Hono): void { + app.get('/environment-build/:environmentId/:buildId/input', async c => { + const auth = await authenticate(c); + if (auth instanceof Response) return auth; + const reply = await coordinator(c, auth.environmentId).runnerInput(auth); + if ('refused' in reply) return refusalResponse(c, reply); + return c.json(reply, 200, { 'cache-control': 'no-store' }); + }); + + app.post('/environment-build/:environmentId/:buildId/events', async c => { + const auth = await authenticate(c); + if (auth instanceof Response) return auth; + let events; + try { + events = environmentBuildEventsSchema.parse(await readJson(c)); + } catch { + return c.json({ error: 'invalid events' }, 400); + } + const reply = await coordinator(c, auth.environmentId).runnerEvents(auth, events); + if ('refused' in reply) return refusalResponse(c, reply); + return c.json(reply); + }); + + app.post('/environment-build/:environmentId/:buildId/result', async c => { + const auth = await authenticate(c); + if (auth instanceof Response) return auth; + let result; + try { + result = environmentBuildResultSchema.parse(await readJson(c)); + } catch { + return c.json({ error: 'invalid result' }, 400); + } + const reply = await coordinator(c, auth.environmentId).runnerResult(auth, result); + if (reply !== 'ok') return refusalResponse(c, reply); + return c.json({ ok: true }); + }); +} diff --git a/services/cloud-agent-next/src/environment-build/runner-credential.ts b/services/cloud-agent-next/src/environment-build/runner-credential.ts new file mode 100644 index 00000000000..b0d3f8848df --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/runner-credential.ts @@ -0,0 +1,63 @@ +import jwt from 'jsonwebtoken'; +import { z } from 'zod'; + +/** + * The build-specific launch credential (plan: "Bind its launch credential and + * every result to environment, build, allocation, and connection identities"). + * It reuses the wrapper launch signing primitive (HS256 with the Worker signing + * secret, a random 32-byte credential whose hash the coordinator stores) with a + * distinct audience and purpose, so a session credential can never act as a + * builder and a builder credential can never reach a Sandbox DO. + */ + +const RUNNER_AUDIENCE = 'cloud-agent-environment-build-runner'; +const RUNNER_CREDENTIAL_MAX_CHARS = 2048; + +const runnerCredentialSchema = z + .object({ + type: z.literal('environment_build_runner'), + aud: z.literal(RUNNER_AUDIENCE), + environmentId: z.string().uuid(), + buildId: z.string().uuid(), + allocationId: z.string().uuid(), + credential: z.string().regex(/^[a-f0-9]{64}$/), + }) + .strict(); + +export type RunnerCredentialClaims = Omit, 'type' | 'aud'>; + +export function mintRunnerCredential(claims: RunnerCredentialClaims, secret: string): string { + return jwt.sign( + runnerCredentialSchema.parse({ + ...claims, + type: 'environment_build_runner', + aud: RUNNER_AUDIENCE, + }), + secret, + { algorithm: 'HS256', noTimestamp: true } + ); +} + +export function verifyRunnerCredential( + token: string, + secret: string +): RunnerCredentialClaims | null { + if (token.length === 0 || token.length > RUNNER_CREDENTIAL_MAX_CHARS) return null; + try { + const parsed = runnerCredentialSchema.safeParse( + jwt.verify(token, secret, { algorithms: ['HS256'], audience: RUNNER_AUDIENCE }) + ); + if (!parsed.success) return null; + const { type: _type, aud: _aud, ...claims } = parsed.data; + return claims; + } catch { + return null; + } +} + +/** The bearer of a builder request, or null for a missing or oversized header. */ +export function parseRunnerBearer(authorization: string | null): string | null { + if (!authorization || authorization.length > RUNNER_CREDENTIAL_MAX_CHARS + 7) return null; + const match = /^Bearer (\S+)$/.exec(authorization); + return match?.[1] ?? null; +} diff --git a/services/cloud-agent-next/src/environment-build/sqlite-schema.ts b/services/cloud-agent-next/src/environment-build/sqlite-schema.ts new file mode 100644 index 00000000000..e01a2d1ff7d --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/sqlite-schema.ts @@ -0,0 +1,130 @@ +import { + index, + integer, + primaryKey, + sqliteTable, + text, + uniqueIndex, +} from 'drizzle-orm/sqlite-core'; + +/** + * Durable SQLite of one `EnvironmentBuild` Durable Object (one per environment + * id). PostgreSQL stays authoritative for configuration and access; this DO owns + * build records, admission, progress, deadlines, cleanup and the published + * snapshot descriptor. Nothing here is mirrored to PostgreSQL. + */ + +export const BUILD_STATES = [ + 'queued', + 'preparing', + 'capturing', + 'publishing', + 'stopping', + 'succeeded', + 'failed', + 'canceled', + 'obsolete', +] as const; +export type BuildState = (typeof BUILD_STATES)[number]; + +export const BUILD_OUTCOMES = ['succeeded', 'failed', 'canceled', 'obsolete'] as const; +export type BuildOutcome = (typeof BUILD_OUTCOMES)[number]; + +export const BUILD_TRIGGERS = ['manual', 'session'] as const; +export type BuildTrigger = (typeof BUILD_TRIGGERS)[number]; + +/** + * One build. `state` is the single execution state; while `stopping`, `outcome` + * holds the terminal state the build enters once cleanup is confirmed. Inputs + * are immutable after admission; the encrypted secrets are cleared once the + * allocation is gone. + */ +export const builds = sqliteTable( + 'builds', + { + id: text('id').primaryKey(), + request_id: text('request_id').notNull(), + revision: integer('revision').notNull(), + trigger: text('trigger', { enum: BUILD_TRIGGERS }).notNull(), + actor_user_id: text('actor_user_id').notNull(), + /** JSON: reference, repository path, commands and plaintext variables. */ + input: text('input').notNull(), + /** JSON envelopes; null once the builder allocation ended. */ + encrypted_secrets: text('encrypted_secrets'), + instance: text('instance').notNull(), + /** SHA-256 of the selected runtime release descriptor; null for the baked image. */ + runtime_release: text('runtime_release'), + state: text('state', { enum: BUILD_STATES }).notNull(), + outcome: text('outcome', { enum: BUILD_OUTCOMES }), + failure_reason: text('failure_reason'), + failure_detail: text('failure_detail'), + /** Builder allocation; written before the provider request is issued. */ + allocation_id: text('allocation_id'), + credential_hash: text('credential_hash'), + launched_at: integer('launched_at'), + last_heartbeat_at: integer('last_heartbeat_at'), + last_event_sequence: integer('last_event_sequence'), + runner_step: text('runner_step'), + source_commit: text('source_commit'), + source_branch: text('source_branch'), + setup_duration_ms: integer('setup_duration_ms'), + /** Written before the capture request; the capture result is read back by it. */ + capture_id: text('capture_id'), + /** JSON of the stored capture result. */ + capture_result: text('capture_result'), + cleanup_attempts: integer('cleanup_attempts').notNull(), + cleanup_error: text('cleanup_error'), + next_cleanup_at: integer('next_cleanup_at'), + log_seq: integer('log_seq').notNull(), + log_bytes: integer('log_bytes').notNull(), + log_truncated: integer('log_truncated', { mode: 'boolean' }).notNull(), + /** JSON map of stage name to epoch ms. */ + stage_times: text('stage_times').notNull(), + created_at: integer('created_at').notNull(), + deadline_at: integer('deadline_at').notNull(), + finished_at: integer('finished_at'), + }, + table => [ + uniqueIndex('builds_request_id').on(table.request_id), + index('builds_state').on(table.state), + index('builds_created_at').on(table.created_at), + ] +); + +/** Redacted output lines of one build, bounded per build. */ +export const buildLogs = sqliteTable( + 'build_logs', + { + build_id: text('build_id').notNull(), + seq: integer('seq').notNull(), + text: text('text').notNull(), + }, + table => [primaryKey({ columns: [table.build_id, table.seq] })] +); + +/** + * The single published snapshot descriptor (`id = 'current'`). It is replaced + * atomically by a successful publication and survives history pruning. + */ +export const publishedSnapshot = sqliteTable('published_snapshot', { + id: text('id').primaryKey(), + descriptor: text('descriptor').notNull(), +}); + +/** Automatic-build backoff per configuration revision. */ +export const autoAttempts = sqliteTable('auto_attempts', { + revision: integer('revision').primaryKey(), + failures: integer('failures').notNull(), + last_failure_at: integer('last_failure_at'), + last_admitted_at: integer('last_admitted_at'), +}); + +/** + * The environment this DO serves (`id = 'current'`). A recorded deletion stops + * every later admission and publication, so a late callback cannot recreate it. + */ +export const environmentRecord = sqliteTable('environment', { + id: text('id').primaryKey(), + environment_id: text('environment_id').notNull(), + deleted_at: integer('deleted_at'), +}); diff --git a/services/cloud-agent-next/src/environment-build/store.ts b/services/cloud-agent-next/src/environment-build/store.ts new file mode 100644 index 00000000000..d6d94007066 --- /dev/null +++ b/services/cloud-agent-next/src/environment-build/store.ts @@ -0,0 +1,327 @@ +import { and, asc, desc, eq, gt, inArray, lt, or } from 'drizzle-orm'; +import type { DrizzleSqliteDODatabase } from 'drizzle-orm/durable-sqlite'; +import { z } from 'zod'; +import { + autoAttempts, + buildLogs, + builds, + environmentRecord, + publishedSnapshot, + type BuildOutcome, + type BuildState, +} from './sqlite-schema.js'; +import { + BUILD_HISTORY_RETENTION_MS, + MAX_BUILD_LOG_BYTES, + MAX_TERMINAL_BUILDS, + publishedSnapshotSchema, + type AutoAttempt, + type PublishedSnapshot, +} from './policy.js'; +import { environmentReferenceSchema } from './reference.js'; + +/** + * Synchronous Durable SQLite access for one `EnvironmentBuild` DO. Every write + * that must be atomic runs in one call here, never across provider I/O. + */ + +export type BuildRow = typeof builds.$inferSelect; +export type BuildPatch = Partial>; + +export const TERMINAL_STATES: readonly BuildState[] = [ + 'succeeded', + 'failed', + 'canceled', + 'obsolete', +]; +const ACTIVE_STATES: readonly BuildState[] = [ + 'queued', + 'preparing', + 'capturing', + 'publishing', + 'stopping', +]; + +export function isTerminal(state: BuildState): state is BuildOutcome { + return (TERMINAL_STATES as readonly string[]).includes(state); +} + +export const buildInputRecordSchema = z + .object({ + reference: environmentReferenceSchema, + repoFullName: z.string().min(1), + commands: z.array(z.string()), + plainVars: z.record(z.string(), z.string()), + }) + .strict(); +export type BuildInputRecord = z.infer; + +const LOG_TRUNCATED_MARKER = '[build log truncated: later output was not kept]'; +const LOG_ROWS_PER_INSERT = 30; + +export class BuildStore { + constructor(private readonly db: DrizzleSqliteDODatabase) {} + + activeBuild(): BuildRow | null { + return ( + this.db + .select() + .from(builds) + .where(inArray(builds.state, [...ACTIVE_STATES])) + .orderBy(desc(builds.created_at)) + .get() ?? null + ); + } + + build(buildId: string): BuildRow | null { + return this.db.select().from(builds).where(eq(builds.id, buildId)).get() ?? null; + } + + buildByRequest(requestId: string): BuildRow | null { + return this.db.select().from(builds).where(eq(builds.request_id, requestId)).get() ?? null; + } + + latestTerminal(): BuildRow | null { + return ( + this.db + .select() + .from(builds) + .where(inArray(builds.state, [...TERMINAL_STATES])) + .orderBy(desc(builds.created_at)) + .get() ?? null + ); + } + + insertBuild(row: typeof builds.$inferInsert): void { + this.db.insert(builds).values(row).run(); + } + + /** + * Compare-and-set: apply `patch` only while the build is still in one of + * `from`. Concurrent RPCs interleave with alarm work across awaits, so every + * transition re-checks the state it expects. + */ + transition(buildId: string, from: readonly BuildState[], patch: BuildPatch): BuildRow | null { + const updated = this.db + .update(builds) + .set(patch) + .where(and(eq(builds.id, buildId), inArray(builds.state, [...from]))) + .returning() + .get(); + return updated ?? null; + } + + update(buildId: string, patch: BuildPatch): void { + this.db.update(builds).set(patch).where(eq(builds.id, buildId)).run(); + } + + /** + * Append redacted lines within the per-build byte bound. Past the bound one + * truncation marker is kept and later lines are dropped. + */ + appendLogs(build: BuildRow, lines: readonly string[]): void { + let seq = build.log_seq; + let bytes = build.log_bytes; + let truncated = build.log_truncated; + const rows: Array = []; + for (const line of lines) { + if (truncated) break; + const size = new TextEncoder().encode(line).byteLength; + if (bytes + size > MAX_BUILD_LOG_BYTES) { + truncated = true; + rows.push({ build_id: build.id, seq: ++seq, text: LOG_TRUNCATED_MARKER }); + break; + } + bytes += size; + rows.push({ build_id: build.id, seq: ++seq, text: line }); + } + if (rows.length === 0) return; + // Durable Object SQLite binds at most 100 variables per statement; a row + // has three. + for (let index = 0; index < rows.length; index += LOG_ROWS_PER_INSERT) { + this.db + .insert(buildLogs) + .values(rows.slice(index, index + LOG_ROWS_PER_INSERT)) + .run(); + } + this.update(build.id, { log_seq: seq, log_bytes: bytes, log_truncated: truncated }); + } + + readLogs(buildId: string, cursor: number, limit: number): Array<{ seq: number; text: string }> { + return this.db + .select({ seq: buildLogs.seq, text: buildLogs.text }) + .from(buildLogs) + .where(and(eq(buildLogs.build_id, buildId), gt(buildLogs.seq, cursor))) + .orderBy(asc(buildLogs.seq)) + .limit(limit) + .all(); + } + + readRecentLogs(buildId: string, limit: number): Array<{ seq: number; text: string }> { + return this.db + .select({ seq: buildLogs.seq, text: buildLogs.text }) + .from(buildLogs) + .where(eq(buildLogs.build_id, buildId)) + .orderBy(desc(buildLogs.seq)) + .limit(limit) + .all() + .reverse(); + } + + published(): PublishedSnapshot | null { + const row = this.db + .select() + .from(publishedSnapshot) + .where(eq(publishedSnapshot.id, 'current')) + .get(); + if (!row) return null; + const parsed = publishedSnapshotSchema.safeParse(JSON.parse(row.descriptor)); + return parsed.success ? parsed.data : null; + } + + /** Replace the published descriptor; the previous one is gone in the same write. */ + publish(descriptor: PublishedSnapshot): void { + const value = JSON.stringify(publishedSnapshotSchema.parse(descriptor)); + this.db + .insert(publishedSnapshot) + .values({ id: 'current', descriptor: value }) + .onConflictDoUpdate({ target: publishedSnapshot.id, set: { descriptor: value } }) + .run(); + } + + autoAttempt(revision: number): AutoAttempt | null { + const row = this.db + .select() + .from(autoAttempts) + .where(eq(autoAttempts.revision, revision)) + .get(); + if (!row) return null; + return { + failures: row.failures, + lastFailureAt: row.last_failure_at, + lastAdmittedAt: row.last_admitted_at, + }; + } + + recordAutoAdmitted(revision: number, now: number): void { + this.db + .insert(autoAttempts) + .values({ revision, failures: 0, last_failure_at: null, last_admitted_at: now }) + .onConflictDoUpdate({ target: autoAttempts.revision, set: { last_admitted_at: now } }) + .run(); + } + + recordAutoFailure(revision: number, now: number): void { + const current = this.autoAttempt(revision); + this.db + .insert(autoAttempts) + .values({ revision, failures: 1, last_failure_at: now, last_admitted_at: null }) + .onConflictDoUpdate({ + target: autoAttempts.revision, + set: { failures: (current?.failures ?? 0) + 1, last_failure_at: now }, + }) + .run(); + } + + recordAutoSuccess(revision: number): void { + this.db + .update(autoAttempts) + .set({ failures: 0, last_failure_at: null }) + .where(eq(autoAttempts.revision, revision)) + .run(); + } + + environment(): { environmentId: string; deletedAt: number | null } | null { + const row = this.db + .select() + .from(environmentRecord) + .where(eq(environmentRecord.id, 'current')) + .get(); + return row ? { environmentId: row.environment_id, deletedAt: row.deleted_at } : null; + } + + /** Remember the environment this DO serves; a different id is a routing defect. */ + bindEnvironment(environmentId: string): void { + const current = this.environment(); + if (current !== null) { + if (current.environmentId !== environmentId) { + throw new Error('Environment build coordinator serves another environment'); + } + return; + } + this.db + .insert(environmentRecord) + .values({ id: 'current', environment_id: environmentId, deleted_at: null }) + .run(); + } + + markDeleted(now: number): void { + this.db + .update(environmentRecord) + .set({ deleted_at: now }) + .where(eq(environmentRecord.id, 'current')) + .run(); + } + + /** + * Bound history: keep at most `MAX_TERMINAL_BUILDS` terminal records, none + * older than the retention window, and their logs. Active builds and the + * published descriptor are never pruned. + */ + prune(now: number): void { + const terminal = this.db + .select({ id: builds.id, created_at: builds.created_at }) + .from(builds) + .where(inArray(builds.state, [...TERMINAL_STATES])) + .orderBy(desc(builds.created_at)) + .all(); + const keep = new Set( + terminal + .filter( + (row, index) => + index < MAX_TERMINAL_BUILDS && now - row.created_at < BUILD_HISTORY_RETENTION_MS + ) + .map(row => row.id) + ); + const remove = terminal.filter(row => !keep.has(row.id)).map(row => row.id); + if (remove.length === 0) return; + for (let index = 0; index < remove.length; index += 50) { + const chunk = remove.slice(index, index + 50); + this.db.delete(buildLogs).where(inArray(buildLogs.build_id, chunk)).run(); + this.db.delete(builds).where(inArray(builds.id, chunk)).run(); + } + } + + /** Builds whose stop is due for another attempt, oldest first. */ + dueCleanups(now: number): BuildRow[] { + return this.db + .select() + .from(builds) + .where( + and( + eq(builds.state, 'stopping'), + or(lt(builds.next_cleanup_at, now + 1), eq(builds.cleanup_attempts, 0)) + ) + ) + .orderBy(asc(builds.created_at)) + .all(); + } + + /** Secret envelopes are kept only while an allocation may still need them. */ + clearSecrets(buildId: string): void { + this.update(buildId, { encrypted_secrets: null }); + } +} + +export function parseBuildInput(row: BuildRow): BuildInputRecord { + return buildInputRecordSchema.parse(JSON.parse(row.input)); +} + +export function stageTimes(row: BuildRow): Record { + const parsed = z.record(z.string(), z.number()).safeParse(JSON.parse(row.stage_times)); + return parsed.success ? parsed.data : {}; +} + +export function withStage(row: BuildRow, stage: string, at: number): string { + return JSON.stringify({ ...stageTimes(row), [stage]: at }); +} diff --git a/services/cloud-agent-next/src/index.ts b/services/cloud-agent-next/src/index.ts index 3e04a9c0bb5..af7b30029c6 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 { EnvironmentBuild } from './environment-build/environment-build-do.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/persistence/session-metadata.ts b/services/cloud-agent-next/src/persistence/session-metadata.ts index c6e42acd147..32018c35c54 100644 --- a/services/cloud-agent-next/src/persistence/session-metadata.ts +++ b/services/cloud-agent-next/src/persistence/session-metadata.ts @@ -1,3 +1,4 @@ +import { environmentReferenceSchema } from '../environment-build/reference.js'; import * as z from 'zod'; import { egressPolicySchema } from '@kilocode/worker-utils/egress-allowlist'; import { @@ -439,6 +440,8 @@ export const CurrentSessionMetadataSchema = z agent: MetadataAgentSchema.optional(), finalization: MetadataFinalizationSchema.optional(), profile: SessionProfileBundleSchema.optional(), + /** The repository environment this session admitted, for snapshot selection. */ + environment: environmentReferenceSchema.optional(), callback: MetadataCallbackSchema.optional(), workspace: MetadataWorkspaceSchema.optional(), devcontainer: MetadataDevContainerSchema.optional(), diff --git a/services/cloud-agent-next/src/router.ts b/services/cloud-agent-next/src/router.ts index aa2c419a02a..945514e9477 100644 --- a/services/cloud-agent-next/src/router.ts +++ b/services/cloud-agent-next/src/router.ts @@ -18,12 +18,24 @@ import { createSessionWorktreeChangesHandlers } from './router/handlers/session- import { getSandboxSelectionOptions } from './router/handlers/sandbox-selection.js'; import { getPreviewOptions } from './router/handlers/preview-options.js'; import { listHostedSessions } from './router/handlers/hosted-session-list.js'; +import { + buildEnvironment, + cancelEnvironmentBuild, + getEnvironmentBuildLogs, + getEnvironmentBuildStatus, + notifyEnvironmentDeleted, +} from './router/handlers/environment-build.js'; export const appRouter = router({ deleteWorktree, getSandboxSelectionOptions, getPreviewOptions, listHostedSessions, + buildEnvironment, + getEnvironmentBuildStatus, + getEnvironmentBuildLogs, + cancelEnvironmentBuild, + notifyEnvironmentDeleted, ...createSessionManagementHandlers(), ...createSessionPrepareHandlers(), ...createSessionExecutionV2Handlers(), diff --git a/services/cloud-agent-next/src/router/handlers/environment-build.ts b/services/cloud-agent-next/src/router/handlers/environment-build.ts new file mode 100644 index 00000000000..b7b490f1cb9 --- /dev/null +++ b/services/cloud-agent-next/src/router/handlers/environment-build.ts @@ -0,0 +1,171 @@ +import { TRPCError } from '@trpc/server'; +import { z } from 'zod'; +import { readEnvironmentReference, type EnvironmentReference } from '@kilocode/cloud-agent-profile'; +import { validateBalanceOnly } from '../../balance-validation.js'; +import { getPgDb } from '../../db/pg.js'; +import { environmentActorAccess } from '../../environment-build/access.js'; +import { + environmentBuildCancelResultSchema, + environmentBuildLogsSchema, + environmentBuildRequestResultSchema, + environmentBuildStatusSchema, + type EnvironmentBuildRequestResult, +} from '../../shared/environment-build-status.js'; +import { withDORetry } from '../../utils/do-retry.js'; +import type { TRPCContext } from '../../types.js'; +import { protectedProcedure } from '../auth.js'; + +/** + * Environment build operations (plan "Build admission and execution"). Every + * handler authorizes before it reaches the coordinator: the environment must + * exist under the caller's owner, Build, Refresh, Cancel and logs need the + * environment management role, and status needs read access. The web router + * checks the same roles; this is the authoritative boundary. + */ + +const environmentScopeSchema = z + .object({ + environmentId: z.string().uuid(), + kilocodeOrganizationId: z.string().uuid().optional(), + }) + .strict(); + +type Scope = z.infer; +type Access = 'manage' | 'read'; + +async function authorize( + ctx: TRPCContext, + scope: Scope, + required: Access +): Promise { + const db = getPgDb(ctx.env); + const owner: EnvironmentReference['owner'] = scope.kilocodeOrganizationId + ? { type: 'organization', id: scope.kilocodeOrganizationId } + : { type: 'user', id: ctx.userId }; + const reference = await readEnvironmentReference(db, scope.environmentId); + if ( + reference === null || + reference.owner.type !== owner.type || + reference.owner.id !== owner.id + ) { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Environment not found' }); + } + const access = await environmentActorAccess(db, owner, ctx.userId); + if (access === 'none') { + throw new TRPCError({ code: 'NOT_FOUND', message: 'Environment not found' }); + } + if (required === 'manage' && access !== 'manage') { + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'Managing environment builds requires an organization owner or admin', + }); + } + return reference; +} + +function coordinator(ctx: TRPCContext, environmentId: string) { + return () => ctx.env.ENVIRONMENT_BUILD.getByName(environmentId); +} + +export const buildEnvironment = protectedProcedure + .input( + environmentScopeSchema.extend({ + expectedRevision: z.number().int().positive(), + /** Client-generated; a retried request after a lost response reuses it. */ + requestId: z.string().uuid(), + }) + ) + .output(environmentBuildRequestResultSchema) + .mutation(async ({ input, ctx }) => { + const reference = await authorize(ctx, input, 'manage'); + // A manual build fails fast with a specific, recoverable billing reason. + const balance = await validateBalanceOnly(ctx.authToken, input.kilocodeOrganizationId, ctx.env); + if (!balance.success && balance.status === 402) { + return { status: 'rejected' as const, reason: 'billing_blocked' as const }; + } + return withDORetry( + coordinator(ctx, input.environmentId), + async (stub): Promise => + stub.requestBuild({ + environmentId: input.environmentId, + owner: reference.owner, + expectedRevision: input.expectedRevision, + requestId: input.requestId, + trigger: 'manual', + actorUserId: ctx.userId, + }), + 'environmentBuild.request' + ); + }); + +export const getEnvironmentBuildStatus = protectedProcedure + .input(environmentScopeSchema) + .output(environmentBuildStatusSchema) + .query(async ({ input, ctx }) => { + const reference = await authorize(ctx, input, 'read'); + return withDORetry( + coordinator(ctx, input.environmentId), + stub => + stub.status({ + environmentId: input.environmentId, + owner: reference.owner, + configurationRevision: reference.configurationRevision, + }), + 'environmentBuild.status' + ); + }); + +export const getEnvironmentBuildLogs = protectedProcedure + .input( + environmentScopeSchema.extend({ + buildId: z.string().uuid(), + cursor: z.number().int().nonnegative().optional(), + limit: z.number().int().min(1).max(1000).default(500), + }) + ) + .output(environmentBuildLogsSchema) + .query(async ({ input, ctx }) => { + await authorize(ctx, input, 'manage'); + const logs = await withDORetry( + coordinator(ctx, input.environmentId), + stub => + stub.logs({ + environmentId: input.environmentId, + buildId: input.buildId, + cursor: input.cursor, + limit: input.limit, + }), + 'environmentBuild.logs' + ); + if (logs === null) throw new TRPCError({ code: 'NOT_FOUND', message: 'Build not found' }); + return logs; + }); + +export const cancelEnvironmentBuild = protectedProcedure + .input(environmentScopeSchema.extend({ buildId: z.string().uuid() })) + .output(environmentBuildCancelResultSchema) + .mutation(async ({ input, ctx }) => { + await authorize(ctx, input, 'manage'); + return withDORetry( + coordinator(ctx, input.environmentId), + stub => stub.cancel({ environmentId: input.environmentId, buildId: input.buildId }), + 'environmentBuild.cancel' + ); + }); + +/** + * Prompt cleanup after an environment was deleted. The coordinator acts only + * when PostgreSQL no longer has the environment, so the call grants nothing; + * correctness never depends on it arriving. + */ +export const notifyEnvironmentDeleted = protectedProcedure + .input(z.object({ environmentId: z.string().uuid() }).strict()) + .output(z.object({ ok: z.literal(true) })) + .mutation(async ({ input, ctx }) => { + await withDORetry( + coordinator(ctx, input.environmentId), + stub => stub.environmentDeleted({ environmentId: input.environmentId }), + 'environmentBuild.deleted' + ); + return { ok: true as const }; + }); diff --git a/services/cloud-agent-next/src/router/handlers/session-creation-preflight.test.ts b/services/cloud-agent-next/src/router/handlers/session-creation-preflight.test.ts index c79ef03d472..85f73c90dd6 100644 --- a/services/cloud-agent-next/src/router/handlers/session-creation-preflight.test.ts +++ b/services/cloud-agent-next/src/router/handlers/session-creation-preflight.test.ts @@ -60,8 +60,21 @@ import { profileResolutionPolicyForSessionCreateOrigin, } from './session-creation-preflight.js'; -function environmentLoad(layer: Record, sandboxAllocation: string | null = null) { - return { layer, sandboxAllocation }; +function environmentLoad( + layer: Record, + environmentId: string, + sandboxAllocation: string | null = null +) { + return { + layer, + sandboxAllocation, + reference: { + environmentId, + owner: { type: 'user' as const, id: 'user-123' }, + configurationRevision: 3, + repository: { platform: 'github' as const, repoFullName: 'acme/repo' }, + }, + }; } const context = { @@ -423,13 +436,14 @@ describe('session creation preflight', () => { }); describe('repository environment resolution', () => { + const environmentId = '123e4567-e89b-12d3-a456-426614174099'; const githubRepository = { type: 'github' as const, repo: 'acme/repo' }; const webOptions = { createdOnPlatform: 'cloud-agent-web' }; it('resolves the repository environment on an approved origin and merges it under the profile layers', async () => { const layer = { envVars: { FROM_ENV: 'env' }, setupCommands: ['env-command'] }; loadEnvironmentForSessionMock.mockResolvedValueOnce( - environmentLoad(layer, 'cloudflare-containers-standard-4') + environmentLoad(layer, environmentId, 'cloudflare-containers-standard-4') ); mergeProfileConfigurationMock.mockResolvedValueOnce({ envVars: { FROM_ENV: 'env' }, @@ -451,11 +465,12 @@ describe('session creation preflight', () => { expect.objectContaining({ environment: layer }) ); expect(resolved.profile?.resolved?.setupCommands).toEqual(['env-command']); + expect(resolved.environment).toEqual(environmentLoad({}, environmentId).reference); expect(resolved.environmentSandboxAllocation).toBe('cloudflare-containers-standard-4'); }); it('omits the allocation when the environment has none', async () => { - loadEnvironmentForSessionMock.mockResolvedValueOnce(environmentLoad({})); + loadEnvironmentForSessionMock.mockResolvedValueOnce(environmentLoad({}, environmentId)); const resolved = await preflightSessionCreation( request({ repository: githubRepository, options: webOptions }), @@ -464,6 +479,7 @@ describe('session creation preflight', () => { ); expect(loadEnvironmentForSessionMock).toHaveBeenCalledOnce(); + expect(resolved.environment?.environmentId).toBe(environmentId); expect(resolved).not.toHaveProperty('environmentSandboxAllocation'); }); @@ -489,21 +505,23 @@ describe('session creation preflight', () => { db, expect.objectContaining({ environment: undefined }) ); - expect(resolved).not.toHaveProperty('environmentSandboxAllocation'); + expect(resolved.environment).toBeUndefined(); } ); - it('drops a client-supplied allocation', async () => { + it('drops a client-supplied environment reference and allocation', async () => { const resolved = await preflightSessionCreation( request({ repository: githubRepository, options: webOptions, + environment: environmentLoad({}, environmentId).reference, environmentSandboxAllocation: 'vercel-large', }), context, 'prepareSession' ); + expect(resolved.environment).toBeUndefined(); expect(resolved.environmentSandboxAllocation).toBeUndefined(); }); @@ -519,7 +537,7 @@ describe('session creation preflight', () => { db, expect.objectContaining({ environment: undefined }) ); - expect(resolved).not.toHaveProperty('environmentSandboxAllocation'); + expect(resolved.environment).toBeUndefined(); }); it('looks up an organization launch under the organization owner', async () => { @@ -608,12 +626,13 @@ describe('session creation preflight', () => { expect(loadEnvironmentForSessionMock).not.toHaveBeenCalled(); expect(resolved).not.toHaveProperty('environmentSandboxAllocation'); + expect(resolved.environment).toBeUndefined(); }); it('rejects an over-limit merged plaintext value with a fixed message and no value', async () => { const longValue = 'x'.repeat(300); loadEnvironmentForSessionMock.mockResolvedValueOnce( - environmentLoad({ envVars: { LONG_PLAINTEXT: longValue } }) + environmentLoad({ envVars: { LONG_PLAINTEXT: longValue } }, environmentId) ); mergeProfileConfigurationMock.mockResolvedValueOnce({ envVars: { LONG_PLAINTEXT: longValue }, @@ -634,7 +653,9 @@ describe('session creation preflight', () => { it('rejects more than 20 concatenated setup commands', async () => { const setupCommands = Array.from({ length: 21 }, (_, index) => `command ${index}`); - loadEnvironmentForSessionMock.mockResolvedValueOnce(environmentLoad({ setupCommands })); + loadEnvironmentForSessionMock.mockResolvedValueOnce( + environmentLoad({ setupCommands }, environmentId) + ); mergeProfileConfigurationMock.mockResolvedValueOnce({ setupCommands }); await expect( diff --git a/services/cloud-agent-next/src/router/handlers/session-creation-preflight.ts b/services/cloud-agent-next/src/router/handlers/session-creation-preflight.ts index 57a1eb9195b..3948eb03d7a 100644 --- a/services/cloud-agent-next/src/router/handlers/session-creation-preflight.ts +++ b/services/cloud-agent-next/src/router/handlers/session-creation-preflight.ts @@ -248,12 +248,21 @@ export async function resolveEffectiveSessionConfiguration( const resolvedRequest = applyProfileResolution(input, resolved); assertResolvedConfigurationWithinSessionLimits(resolvedRequest.profile?.resolved); - // The allocation travels beside the merged configuration; a client cannot - // set it. - const { environmentSandboxAllocation: _clientAllocation, ...withoutEnvironment } = - resolvedRequest; - if (!environment?.sandboxAllocation) return withoutEnvironment; - return { ...withoutEnvironment, environmentSandboxAllocation: environment.sandboxAllocation }; + // The reference and the allocation travel beside the merged configuration; + // a client cannot set them. + const { + environment: _clientEnvironment, + environmentSandboxAllocation: _clientAllocation, + ...withoutEnvironment + } = resolvedRequest; + if (!environment) return withoutEnvironment; + return { + ...withoutEnvironment, + environment: environment.reference, + ...(environment.sandboxAllocation + ? { environmentSandboxAllocation: environment.sandboxAllocation } + : {}), + }; } export function assertModeAvailableForProfile(mode: string, profile: SessionProfileBundle): void { diff --git a/services/cloud-agent-next/src/router/handlers/session-prepare.test.ts b/services/cloud-agent-next/src/router/handlers/session-prepare.test.ts index 37f5ddbf002..42320c9752e 100644 --- a/services/cloud-agent-next/src/router/handlers/session-prepare.test.ts +++ b/services/cloud-agent-next/src/router/handlers/session-prepare.test.ts @@ -22,8 +22,21 @@ import { } from './session-prepare.js'; import { createSessionStartHandlers } from './session-start.js'; -function environmentLoad(layer: Record, sandboxAllocation: string | null = null) { - return { layer, sandboxAllocation }; +function environmentLoad( + layer: Record, + environmentId: string, + sandboxAllocation: string | null = null +) { + return { + layer, + sandboxAllocation, + reference: { + environmentId, + owner: { type: 'user' as const, id: 'user-123' }, + configurationRevision: 3, + repository: { platform: 'github' as const, repoFullName: 'acme/repo' }, + }, + }; } const { @@ -852,8 +865,9 @@ describe('prepareSession operation-ledger admission gate', () => { it('carries the repository environment and its merged configuration in the registration-only create request', async () => { const caller = router.createCaller(createContext()); + const environmentId = '123e4567-e89b-12d3-a456-426614174099'; const layer = { envVars: { ENV_ONLY: 'from-environment' }, setupCommands: ['env-setup'] }; - loadEnvironmentForSessionMock.mockResolvedValueOnce(environmentLoad(layer)); + loadEnvironmentForSessionMock.mockResolvedValueOnce(environmentLoad(layer, environmentId)); mergeProfileConfigurationMock.mockResolvedValueOnce(layer); await caller.prepareSession({ @@ -871,6 +885,7 @@ describe('prepareSession operation-ledger admission gate', () => { ); expect(registerNewSessionMock).toHaveBeenCalledWith( expect.objectContaining({ + environment: environmentLoad({}, environmentId).reference, profile: expect.objectContaining({ resolved: expect.objectContaining({ envVars: { ENV_ONLY: 'from-environment' }, 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..fff5e4d25df 100644 --- a/services/cloud-agent-next/src/router/handlers/session-worktree.ts +++ b/services/cloud-agent-next/src/router/handlers/session-worktree.ts @@ -422,6 +422,7 @@ function buildRegistrationInput( agent: source.metadata.agent, repository, ...(source.metadata.profile ? { profile: source.metadata.profile } : {}), + ...(source.metadata.environment ? { environment: source.metadata.environment } : {}), ...(source.metadata.finalization ? { finalization: source.metadata.finalization } : {}), workspace, lifecycle: { version: now, timestamp: now }, diff --git a/services/cloud-agent-next/src/sandbox-containers/SandboxContainers.ts b/services/cloud-agent-next/src/sandbox-containers/SandboxContainers.ts index 133f7025c4c..11e1d8f3160 100644 --- a/services/cloud-agent-next/src/sandbox-containers/SandboxContainers.ts +++ b/services/cloud-agent-next/src/sandbox-containers/SandboxContainers.ts @@ -1,19 +1,19 @@ +import { + MAX_SNAPSHOT_START_FAILURES, + SNAPSHOT_START_FAILURES_KEY, + parseSnapshotStartFailures, + recordSnapshotStartFailure, +} from './snapshot-start-failures.js'; import { getBillingContext } from '@kilocode/container-usage'; import { withTimeout } from '@kilocode/worker-utils'; import { DurableObject } from 'cloudflare:workers'; import { billingHeartbeatSeconds } from '../container-usage.js'; import { resolveControlPlaneTimers } from '../shared/control-plane-timers.js'; import { logger } from '../logger.js'; -import { - CONTAINERS_INTERCEPT_CA_PATH, - SANDBOX_INTERCEPT_HTTPS_ENABLED, - SANDBOX_INTERCEPT_HTTPS_ENV, -} from '../shared/container-intercept.js'; import { CONTROL_PROCESS_MATCH, CONTROL_WRAPPER_LOG_PATH, } from '../sandbox-control/container-paths.js'; -import { CONTROL_PLANE_NATIVE_LOGS_ENV } from '../shared/control-diagnostics.js'; import { DEADLINE_MS } from '../sandbox-control/deadlines.js'; import { diagnosticCause, @@ -22,6 +22,10 @@ import { } from '../sandbox-control/diagnostics.js'; import { fetchPreviewPort, previewNotRunningResponse } from '../preview/container-port-fetch.js'; import { isReservedPreviewPort } from '../shared/reserved-ports.js'; +import { + ENVIRONMENT_BUILD_RUNNER_DELIVERY_SCRIPT, + type EnvironmentBuildRunner, +} from '../shared/environment-build-protocol.js'; import { PREVIEW_MARKER_ALLOCATION_HEADER, PREVIEW_MARKER_PORT_HEADER, @@ -33,18 +37,31 @@ import { unavailableContainersBillingAdmission, type ContainersBillingHost, } from './containers-billing.js'; -import { - createRepoSnapshotIndex, - repoSnapshotIndexKey, - REPO_SNAPSHOT_MAX_START_FAILURES, - type RepoSnapshotIndex, -} from './repo-snapshot-index.js'; import { mainProcessObservationFields, observeCmdline, type MainProcessIdentity, type MainProcessObservation, + type NativeMainProcess, } from './main-process.js'; +import { + nativeLaunchEnv, + planNativeStart, + type ContainerInstanceSize, + type ContainersStartSource, + type NativeStartPlan, + type NativeStartSnapshot, +} from './native-start.js'; +import { + CAPTURE_INDEX_KEY, + captureKey, + captureResult, + parseCaptureIndex, + parseCaptureRecord, + retainCaptureIds, + type CaptureRecord, + type NativeCaptureResult, +} from './native-capture.js'; import type { Env } from '../types.js'; import type { ProviderStartupCommand } from '../sandbox-control/provider.js'; import { @@ -53,15 +70,11 @@ import { type NativeEgressPolicy, } from './egress-policy.js'; -import { containerInstanceStartValue, type ContainerInstanceSize } from './instance-size.js'; - -export type { ContainerInstanceSize } from './instance-size.js'; +export type { ContainerInstanceSize, ContainersStartSource } from './native-start.js'; +export type { NativeCaptureResult } from './native-capture.js'; export type ContainersState = 'idle' | 'launching' | 'running' | 'stopping'; -/** What a physical start used: the image, or a repository snapshot of it. */ -export type ContainersStartSource = 'image' | 'repository'; - export type ContainersLaunchInput = { allocationRef: string; command: ProviderStartupCommand; @@ -69,13 +82,28 @@ export type ContainersLaunchInput = { instance: ContainerInstanceSize; containment?: boolean; egressAllowlist?: string[]; - /** Keyed hash of the launch's scope, repository and env; absent means no snapshot. */ - repoKey?: string; - /** Start from the image and retain the failed snapshot for debugging. */ - skipRepository?: true; + /** Environment snapshot selected for this allocation; absent starts from the image. */ + snapshot?: NativeStartSnapshot; deadlineAt?: number; }; +/** + * An environment builder start. The container DO named for one environment runs + * only builders, from the image, with internet egress and no interception: the + * builder holds a build-scoped repository credential, not contained aliases. + */ +const BUILDER_RUNNER_ENV = 'KILO_BUILDER_RUNNER'; +/** Atomic, owner-only write of the runner file; the value comes from exec env. */ + +export type ContainersBuilderLaunchInput = { + allocationRef: string; + /** Written to the running builder by exec; the builder starts without it. */ + runner: EnvironmentBuildRunner; + instance: ContainerInstanceSize; + command: ProviderStartupCommand; + deadlineAt: number; +}; + export type ContainersLaunchResult = { started: boolean; startSource: ContainersStartSource; @@ -113,17 +141,18 @@ type ContainersRecord = { instance?: ContainerInstanceSize; billingConfigured?: true; wrapperAttempt?: WrapperAttempt; - repositoryStartFailures?: { snapshotId: string; attempts: number }; egressPolicy?: NativeEgressPolicy; }; -type ResolvedStart = { - image: string; - source: ContainersStartSource; - options: ContainerStartupOptions; - snapshotId: string | null; +/** A start plan plus what the issued main process must be and how long it may take. */ +type ResolvedStart = NativeStartPlan & { + mainProcess: NativeMainProcess; + command: ProviderStartupCommand; }; +/** A start that did not reach its main process, reported to the caller instead of thrown. */ +type StartFailure = { snapshotFailed: true }; + type DelayedSchedule = { taskId: string; callback: string; @@ -136,34 +165,21 @@ type DelayedSchedule = { const RECORD_KEY = 'containers:record:v1'; const CONTAINER_IMAGE = 'app'; -function interceptedProcessEnv(env: Record): Record { - // Bun reads NODE_EXTRA_CA_CERTS only at process start, so the injected CA file must be - // readable before this exec for the wrapper's own TLS; cert.ts only completes the bundle - // append and the child env afterwards. - return { - ...env, - [SANDBOX_INTERCEPT_HTTPS_ENV]: SANDBOX_INTERCEPT_HTTPS_ENABLED, - NODE_EXTRA_CA_CERTS: CONTAINERS_INTERCEPT_CA_PATH, - }; -} - const PROBE_TIMEOUT_MS = 5_000; const CONTAINER_CALL_TIMEOUT_MS = 5_000; /** Pause between readiness probes, so repeated pgrep stays sequential and bounded. */ const WRAPPER_READINESS_POLL_MS = 1_000; -/** Bounds one repository capture. The wrapper's own wait is a backstop over this. */ -const REPO_CAPTURE_TIMEOUT_MS = 5 * 60_000; +/** Bounds one snapshot capture. The environment coordinator's wait is a backstop over this. */ +export const SNAPSHOT_CAPTURE_TIMEOUT_MS = 5 * 60_000; const DESTROY_TIMEOUT_MS = 30_000; -type RepositoryCaptureOutcome = 'stored' | 'index_unavailable' | 'abandoned' | 'failed'; - /** * One line per capture, with how long the platform snapshot took and its size * when the platform returned one, so the capture bound can be tuned from real - * durations. It carries no key, id or credential. + * durations. It carries no snapshot id or credential. */ -function logRepositoryCapture( - outcome: RepositoryCaptureOutcome, +function logSnapshotCapture( + outcome: CaptureRecord['state'] | Extract['reason'], durationMs: number, details: { size?: number; error?: unknown } = {} ): void { @@ -174,8 +190,8 @@ function logRepositoryCapture( ...(size === undefined ? {} : { size }), ...(error === undefined ? {} : { error: error instanceof Error ? error.message : 'unknown' }), }); - if (outcome === 'stored') fields.info('Repository snapshot captured'); - else fields.warn('Repository snapshot not saved'); + if (outcome === 'stored') fields.info('Environment snapshot captured'); + else fields.warn('Environment snapshot not captured'); } const MAX_LOG_BYTES = 1024 * 1024; @@ -242,7 +258,8 @@ export class SandboxContainers extends DurableObject { private queue: Promise = Promise.resolve(); private billing: ContainersBilling | undefined; private schedules: ContainersBillingScheduler | undefined; - private repoSnapshots: RepoSnapshotIndex | null | undefined; + /** Snapshot calls still awaited in this isolate, by capture id. */ + private readonly inflightCaptures = new Map>(); constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); @@ -370,20 +387,111 @@ export class SandboxContainers extends DurableObject { if (input.containment === true || input.egressAllowlist !== undefined) { await this.installOutboundProxy(container, input.egressAllowlist); } - const start = await this.resolveStart(input, true); - return this.issueNativeSupervisor(record, ref, start, input); + const start = await this.resolveStart(input, 'supervisor', true); + return this.issueNativeMainProcess(record, ref, start, input.egressAllowlist, input.deadlineAt); } /** - * Issue the supplied command as PID 1: fence, start, confirm identity, mark + * Start an environment builder for `allocationRef`. Only the environment + * coordinator calls this, on the container DO named for its environment, so a + * record held by another allocation is a previous builder and is destroyed + * first. A repeated call for the running builder retries runner delivery; a builder whose + * start may still be in flight is confirmed by its main-process identity. + */ + async launchBuilder(input: ContainersBuilderLaunchInput): Promise { + return this.runExclusive(async () => { + const ref = input.allocationRef; + let stored = await this.readRecord(); + if (stored.allocationRef === ref && stored.state === 'stopping') { + throw new ContainersAllocationConflictError(ref); + } + if (stored.allocationRef !== null && stored.allocationRef !== ref) { + stored = await this.releaseSupersededAllocation(stored); + } + if (stored.allocationRef === ref && stored.state === 'running') { + await this.deliverBuilderRunner(this.requiredContainer(), input.runner, input.deadlineAt); + return { started: false, startSource: 'image' }; + } + const container = this.requiredContainer(); + if (stored.allocationRef === ref && stored.state === 'launching') { + if (container.running !== true) { + throw new Error('Builder start is pending and the container is not running'); + } + if ( + (await this.settleRunningMainProcess( + stored, + ref, + 'environment-builder', + input.command + )) !== 'issued' + ) { + throw new Error('Builder container main process is not the environment builder'); + } + await this.deliverBuilderRunner(container, input.runner, input.deadlineAt); + return { started: true, startSource: 'image' }; + } + if (Date.now() >= input.deadlineAt) throw new WrapperExecTimeoutError(); + if (container.running) { + throw new Error('Cannot start a builder on an unowned running container'); + } + const record = await this.installLaunchInstance(stored, input.instance); + const start = await this.resolveStart( + { + env: {}, + instance: input.instance, + command: input.command, + }, + 'environment-builder', + false + ); + const result = await this.issueNativeMainProcess( + record, + ref, + start, + undefined, + input.deadlineAt + ); + if (result.started) { + await this.deliverBuilderRunner(container, input.runner, input.deadlineAt); + } + return result; + }); + } + + /** + * Hand the started builder its endpoint and credential. Exec env is not part + * of the start configuration a container snapshot keeps, so a session + * restored from the snapshot never inherits them; the builder deletes the + * file once read. + */ + private async deliverBuilderRunner( + container: Container, + runner: EnvironmentBuildRunner, + deadlineAt: number + ): Promise { + if (Date.now() >= deadlineAt) throw new WrapperExecTimeoutError(); + const proc = await this.awaitContainerCall( + container.exec(['/bin/sh', '-c', ENVIRONMENT_BUILD_RUNNER_DELIVERY_SCRIPT], { + env: { [BUILDER_RUNNER_ENV]: JSON.stringify(runner) }, + cwd: '/', + }), + deadlineAt + ); + const output = await this.awaitContainerCall(proc.output(), deadlineAt); + if (output.exitCode !== 0) throw new Error('Builder runner delivery failed'); + } + + /** + * Issue a native main process as PID 1: fence, start, confirm identity, mark * running. Containment is installed by the caller, which owns its ordering * before any physical call. */ - private async issueNativeSupervisor( + private async issueNativeMainProcess( record: ContainersRecord, ref: string, start: ResolvedStart, - input: ContainersLaunchInput + egressAllowlist: string[] | undefined, + deadlineAt: number | undefined ): Promise { const container = this.requiredContainer(); // Ownership is retained before start: an ambiguous start that takes effect @@ -397,114 +505,73 @@ export class SandboxContainers extends DurableObject { startSource: start.source, startImage: start.image, }; - if (input.egressAllowlist === undefined) { + if (egressAllowlist === undefined) { delete launching.egressPolicy; } else { - launching.egressPolicy = { allocationRef: ref, allowlist: input.egressAllowlist }; + launching.egressPolicy = { allocationRef: ref, allowlist: egressAllowlist }; } await this.writeRecord(launching); - if (input.deadlineAt !== undefined && Date.now() >= input.deadlineAt) { + if (deadlineAt !== undefined && Date.now() >= deadlineAt) { throw new WrapperExecTimeoutError(); } - const started = await this.runStart(start, async () => { + const failure = await this.runStart(start, async () => { const issuedAt = Date.now(); await this.startContainerAndActivateBilling(container, record, start.options); - await this.awaitSupervisorMainProcess( - container, - input.command, - start.source, - issuedAt, - input.deadlineAt - ); - // A pending native start can become billable during the PID 1 read. + await this.awaitMainProcess(container, start, issuedAt, deadlineAt); await this.activateBillingIfRunning(container, record); }); - if (!started) return { started: false, startSource: 'repository', snapshotFailed: true }; + if (failure !== null) return { started: false, startSource: start.source, ...failure }; await this.writeRunning(ref, 'clear'); return { started: true, startSource: start.source }; } /** - * The one start-source rule: a physical start uses the repository snapshot for - * the launch's `repoKey` and this image when the index has one, and the image - * otherwise. Failed snapshots are retained and retried before image fallback. - * - * Emits one `snapshot_lookup` diagnostic per start so `image` vs `repository` - * and the reason (no key, no index, miss, exhausted, skipped, not allowed) are visible. - * A hit adds the snapshot's age and size. Like the capture line it carries no - * repository key; the key is logged by the Sandbox DO, which owns it. + * The start plan for one launch. A session start may use the offered + * environment snapshot; a builder always starts from the image. Emits one + * `snapshot_lookup` diagnostic per start so `image` vs `repository` and the + * reason are visible. Like the capture line it carries no snapshot handle. */ private async resolveStart( - input: ContainersLaunchInput, - allowRepository: boolean + input: Pick< + ContainersLaunchInput, + 'env' | 'instance' | 'containment' | 'egressAllowlist' | 'snapshot' | 'command' + >, + mainProcess: NativeMainProcess, + allowSnapshot: boolean ): Promise { - const image = this.containerImage(); - const imageStart: ResolvedStart = { - image, - source: 'image', - options: this.startOptions( - input.instance, - input.command, - image, - this.nativeLaunchEnv( - input.env, - input.containment === true || input.egressAllowlist !== undefined - ), - input.egressAllowlist - ), - snapshotId: null, - }; - const index = this.repoSnapshotIndex(); - if (index === null) { - this.logSnapshotLookup('image', 'index_unavailable'); - return imageStart; - } - if (input.repoKey === undefined) { - this.logSnapshotLookup('image', 'no_repo_key'); - return imageStart; - } - const indexKey = await repoSnapshotIndexKey(input.repoKey, image); - if (input.skipRepository === true) { - this.logSnapshotLookup('image', 'skipped'); - return imageStart; - } - if (!allowRepository) { - this.logSnapshotLookup('image', 'not_allowed'); - return imageStart; - } - const entry = await index.lookup(indexKey); - if (entry === null) { - this.logSnapshotLookup('image', 'index_miss'); - return imageStart; - } - const failures = (await this.readRecord()).repositoryStartFailures; - if ( - failures?.snapshotId === entry.snapshotId && - failures.attempts >= REPO_SNAPSHOT_MAX_START_FAILURES - ) { - this.logSnapshotLookup('image', 'retries_exhausted', { attempts: failures.attempts }); - return imageStart; - } - // An entry written before `capturedAt` logs no age. - this.logSnapshotLookup('repository', 'index_hit', { - ageMs: entry.capturedAt === undefined ? undefined : Date.now() - entry.capturedAt, - size: entry.size, + const failures = + input.snapshot === undefined + ? [] + : parseSnapshotStartFailures(await this.ctx.storage.get(SNAPSHOT_START_FAILURES_KEY)); + const snapshotRejected = + input.snapshot !== undefined && + (failures.find(record => record.snapshotId === input.snapshot?.snapshotId)?.failures ?? 0) >= + MAX_SNAPSHOT_START_FAILURES; + const plan = planNativeStart( + { + image: this.containerImage(), + instance: input.instance, + mainProcess, + env: input.env, + interception: input.containment === true || input.egressAllowlist !== undefined, + ...(input.egressAllowlist === undefined ? {} : { egressAllowlist: input.egressAllowlist }), + command: input.command, + ...(input.snapshot === undefined ? {} : { snapshot: input.snapshot }), + }, + allowSnapshot && !snapshotRejected + ); + if (snapshotRejected) plan.reason = 'restore_failures'; + logControlDiagnostic('snapshot_lookup', { + sandboxId: this.sandboxNameForLog(), + source: plan.source, + reason: plan.reason, + mainProcess, + downloaded: input.command.argv.length > 2, }); return { - image, - source: 'repository', - options: this.startOptions( - input.instance, - input.command, - image, - this.nativeLaunchEnv( - input.env, - input.containment === true || input.egressAllowlist !== undefined - ), - input.egressAllowlist, - entry.snapshotId - ), - snapshotId: entry.snapshotId, + ...plan, + mainProcess, + command: input.command, }; } @@ -512,53 +579,40 @@ export class SandboxContainers extends DurableObject { return this.ctx.id.name ?? this.ctx.id.toString(); } - private logSnapshotLookup( - source: ContainersStartSource, - reason: string, - fields: ControlDiagnosticFields = {} - ): void { - logControlDiagnostic('snapshot_lookup', { - sandboxId: this.sandboxNameForLog(), - source, - reason, - ...fields, - }); - } - - /** Count failed snapshot starts durably without changing the shared snapshot index. */ - private async runStart(start: ResolvedStart, attempt: () => Promise): Promise { + /** + * Run one physical start. A failed snapshot start is reported, not thrown, so + * the caller can retry the same allocation from the image once. A runtime + * installer that exited with a typed code is reported the same way, so the + * provider maps it to its reason. Every other failure throws. + */ + private async runStart( + start: ResolvedStart, + attempt: () => Promise + ): Promise { try { await attempt(); - return true; + return null; } catch (error) { if (start.source === 'repository' && start.snapshotId !== null) { - const record = await this.readRecord(); - const previous = record.repositoryStartFailures; - const attempts = (previous?.snapshotId === start.snapshotId ? previous.attempts : 0) + 1; - await this.writeRecord({ - ...record, - repositoryStartFailures: { snapshotId: start.snapshotId, attempts }, - }); + const failures = parseSnapshotStartFailures( + await this.ctx.storage.get(SNAPSHOT_START_FAILURES_KEY) + ); + await this.ctx.storage.put( + SNAPSHOT_START_FAILURES_KEY, + recordSnapshotStartFailure(failures, start.snapshotId) + ); logControlDiagnostic('snapshot_start_failed', { sandboxId: this.sandboxNameForLog(), reason: 'repository_start_failed', - attempts, errorName: error instanceof Error ? diagnosticCause(error.name) : 'unknown', cause: error instanceof Error ? diagnosticCause(error.message) : 'unknown', }); - return false; + return { snapshotFailed: true }; } throw error; } } - private repoSnapshotIndex(): RepoSnapshotIndex | null { - if (this.repoSnapshots === undefined) { - this.repoSnapshots = createRepoSnapshotIndex(this.env.REPO_SNAPSHOTS); - } - return this.repoSnapshots; - } - async observe(_allocationRef: string): Promise { const record = await this.readRecord(); return { @@ -788,10 +842,22 @@ export class SandboxContainers extends DurableObject { const interception = input.containment === true || input.egressAllowlist !== undefined; if (interception) await this.installOutboundProxy(container, input.egressAllowlist); if (!container.running) { - const start = await this.resolveStart(input, record.startSource === 'repository'); - return this.issueNativeSupervisor(record, ref, start, input); + const start = await this.resolveStart( + input, + 'supervisor', + record.startSource === 'repository' + ); + return this.issueNativeMainProcess( + record, + ref, + start, + input.egressAllowlist, + input.deadlineAt + ); } - if ((await this.settleRunningMainProcess(record, ref, input.command)) === 'started') { + if ( + (await this.settleRunningMainProcess(record, ref, 'supervisor', input.command)) === 'issued' + ) { return { started: true, startSource: recordedStartSource(record) }; } // The image default `sleep infinity` still owns the container; the wrapper @@ -818,28 +884,28 @@ export class SandboxContainers extends DurableObject { } /** - * Settle the PID 1 identity of an already-running container. A verified - * supervisor completes issuance (billing + running); an ambiguous identity - * meters the stored generation and throws. The image default `sleep infinity` - * returns to the caller, which owns the sleep-path probe/exec. + * Settle the PID 1 identity of an already-running container. The expected + * main process completes issuance (billing + running); an ambiguous or + * different identity meters the stored generation and throws. The image + * default `sleep infinity` returns to the caller, which owns the sleep-path + * probe/exec. */ private async settleRunningMainProcess( record: ContainersRecord, ref: string, + expected: NativeMainProcess, command: ProviderStartupCommand - ): Promise<'started' | 'sleep'> { + ): Promise<'issued' | 'sleep'> { const container = this.requiredContainer(); const identity = await this.readMainProcessIdentity(container, command); - if (identity === 'ambiguous') { - await this.activateBillingIfRunning(container, record); - throw new Error('Main process identity is ambiguous'); - } - if (identity === 'supervisor' || identity === 'startup') { + if (identity === expected || identity === 'startup') { await this.activateBillingIfRunning(container, record); await this.writeRunning(ref, 'clear'); - return 'started'; + return 'issued'; } - return 'sleep'; + if (identity === 'sleep') return 'sleep'; + await this.activateBillingIfRunning(container, record); + throw new Error('Main process identity is ambiguous'); } /** @@ -864,7 +930,9 @@ export class SandboxContainers extends DurableObject { if (input.containment === true || input.egressAllowlist !== undefined) { await this.installOutboundProxy(container, input.egressAllowlist); } - if ((await this.settleRunningMainProcess(stored, ref, input.command)) === 'started') { + if ( + (await this.settleRunningMainProcess(stored, ref, 'supervisor', input.command)) === 'issued' + ) { return { started: true, startSource: recordedStartSource(stored) }; } const probe = await this.probeWrapper(container); @@ -1025,7 +1093,7 @@ export class SandboxContainers extends DurableObject { if (Date.now() >= deadlineAt) throw new WrapperExecTimeoutError(); return this.awaitContainerCall( container.exec([...command.argv], { - env: this.nativeLaunchEnv(env, containment), + env: nativeLaunchEnv(env, containment), cwd: '/', }), deadlineAt @@ -1084,39 +1152,6 @@ export class SandboxContainers extends DurableObject { return container; } - private startOptions( - instance: ContainerInstanceSize, - command: ProviderStartupCommand, - image: string, - env: Record, - egressAllowlist?: string[], - snapshotId?: string - ): ContainerStartupOptions { - // The supplied command runs first, including on a repository restore. - return { - ...(snapshotId === undefined ? { image } : { containerSnapshot: { id: snapshotId } }), - instance: containerInstanceStartValue(instance), - enableInternet: egressAllowlist === undefined, - entrypoint: [...command.argv], - env, - }; - } - - /** - * The env for a native start and for a sleep-main-process exec. Interception CA - * goes on both; the stderr gate is native-only and set here, never in the - * shared launch-env builder. - */ - private nativeLaunchEnv( - env: Record, - interception: boolean - ): Record { - return { - ...(interception ? interceptedProcessEnv(env) : env), - [CONTROL_PLANE_NATIVE_LOGS_ENV]: '1', - }; - } - private async readMainProcessIdentity( container: Container, command: ProviderStartupCommand @@ -1148,28 +1183,27 @@ export class SandboxContainers extends DurableObject { } /** - * Confirm PID 1 is the supplied command or stable supervisor, bounded by the + * Confirm the native start issued its main process as PID 1, bounded by the * start-source readiness deadline. This is identity, not a wrapper-child * poll: a deadline without a match, or a container that stops first, throws - * and does not exec. + * and does not exec. A runtime installer start is bounded by its own launch + * deadline instead, because the installer runs before the handoff. * * Emits one `supervisor_confirm` diagnostic on a match (time to ready since the * start was issued) or at the deadline. The read in flight at the deadline is * usually cut off, so the timeout reports the last read that completed. */ - private async awaitSupervisorMainProcess( + private async awaitMainProcess( container: Container, - command: ProviderStartupCommand, - source: ContainersStartSource, + start: ResolvedStart, issuedAt: number, launchDeadlineAt?: number ): Promise { - const deadlineAt = Math.min( - source === 'repository' + const readinessDeadline = + start.source === 'repository' ? issuedAt + DEADLINE_MS.repositoryStartup - : Date.now() + DEADLINE_MS.wrapperReadiness, - launchDeadlineAt ?? Infinity - ); + : Date.now() + DEADLINE_MS.wrapperReadiness; + const deadlineAt = Math.min(readinessDeadline, launchDeadlineAt ?? Infinity); let checks = 0; let lastCompleted: MainProcessObservation | null = null; const stopped = container.monitor().then( @@ -1183,10 +1217,8 @@ export class SandboxContainers extends DurableObject { }) ); for (;;) { - // Native exec can reject before startup completes. Only monitor settling - // proves a stop; its failure also wins over a stalled PID 1 read. const result = await Promise.race([ - this.observeMainProcess(container, command, deadlineAt).then(observation => ({ + this.observeMainProcess(container, start.command, deadlineAt).then(observation => ({ type: 'observed' as const, observation, })), @@ -1197,20 +1229,20 @@ export class SandboxContainers extends DurableObject { if (result.type === 'stopped') { this.logSupervisorConfirm( 'stopped', - source, + start, issuedAt, checks, mainProcessObservationFields(observation) ); - throw new Error('Container stopped before the supervisor main process was confirmed'); + throw new Error('Container stopped before the main process was confirmed'); } if (observation.kind !== 'deadline') lastCompleted = observation; - if (observation.identity === 'supervisor' || observation.identity === 'startup') { - this.logSupervisorConfirm('ready', source, issuedAt, checks); + if (observation.identity === start.mainProcess || observation.identity === 'startup') { + this.logSupervisorConfirm('ready', start, issuedAt, checks); return; } if (Date.now() >= deadlineAt) { - this.logSupervisorConfirm('timeout', source, issuedAt, checks, { + this.logSupervisorConfirm('timeout', start, issuedAt, checks, { finalCutOff: observation.kind === 'deadline', ...mainProcessObservationFields(lastCompleted), }); @@ -1222,7 +1254,7 @@ export class SandboxContainers extends DurableObject { private logSupervisorConfirm( result: 'ready' | 'timeout' | 'stopped', - source: ContainersStartSource, + start: ResolvedStart, issuedAt: number, checks: number, fields: ControlDiagnosticFields = {} @@ -1232,7 +1264,9 @@ export class SandboxContainers extends DurableObject { { sandboxId: this.sandboxNameForLog(), result, - source, + source: start.source, + mainProcess: start.mainProcess, + downloaded: start.command.argv.length > 2, elapsedMs: Date.now() - issuedAt, checks, ...fields, @@ -1258,9 +1292,6 @@ export class SandboxContainers extends DurableObject { ...(record.instance !== undefined ? { instance: record.instance } : {}), ...(record.billingConfigured ? { billingConfigured: true } : {}), ...(record.egressPolicy === undefined ? {} : { egressPolicy: record.egressPolicy }), - ...(record.repositoryStartFailures === undefined - ? {} - : { repositoryStartFailures: record.repositoryStartFailures }), }; } @@ -1299,55 +1330,117 @@ export class SandboxContainers extends DurableObject { } /** - * Snapshot the running container's root filesystem as the repository snapshot - * for `repoKey` and its recorded start image. It does not take the operation queue: a capture - * of unknown duration must not delay a stop or a launch. The snapshot is - * published only while `allocationRef` is still the running allocation, and - * any failure is a `false` the caller may ignore; a capture never fails a start. + * Snapshot the running container's root filesystem under `captureId` (plan + * step 8). The outcome is stored under that id before this replies, so a caller + * that lost the reply reads it back with `readCapture`. A repeated call returns + * the stored outcome and never snapshots twice; an issued capture whose call is + * no longer running in this isolate is `uncertain`. It does not take the + * operation queue: a capture of unknown duration must not delay a stop. */ - async captureRepository( + async captureSnapshot(allocationRef: string, captureId: string): Promise { + const key = captureKey(captureId); + const inflight = this.inflightCaptures.get(captureId); + if (inflight) return inflight; + const existing = parseCaptureRecord(await this.ctx.storage.get(key)); + if (existing !== null) return captureResult(existing, false); + const capture = this.runCapture(allocationRef, captureId, key); + this.inflightCaptures.set(captureId, capture); + try { + return await capture; + } finally { + this.inflightCaptures.delete(captureId); + } + } + + /** The stored outcome of one capture, or null when this DO never recorded it. */ + /** The image a launch uses now; environment snapshots must match it. */ + async currentImage(): Promise { + return this.containerImage(); + } + + async readCapture(captureId: string): Promise { + const record = parseCaptureRecord(await this.ctx.storage.get(captureKey(captureId))); + if (record === null) return null; + return captureResult(record, this.inflightCaptures.has(captureId)); + } + + private async runCapture( allocationRef: string, - repoKey: string, - commit?: string - ): Promise { - const index = this.repoSnapshotIndex(); + captureId: string, + key: string + ): Promise { const container = this.ctx.container; - if (index === null || !container || container.running !== true) return false; - const record = await this.readRecord(); if ( - record.state !== 'running' || - record.allocationRef !== allocationRef || - record.startImage === undefined - ) - return false; + !container || + container.running !== true || + !(await this.isRunningAllocation(allocationRef)) + ) { + return this.storeCapture(captureId, key, { + state: 'failed', + allocationRef, + reason: 'not_running', + }); + } + const baseImage = (await this.readRecord()).startImage ?? this.containerImage(); + // Issued evidence goes down before the platform call: after a restart an + // issued record without a result is uncertain, never a reason to retry. + await this.storeCapture(captureId, key, { + state: 'issued', + allocationRef, + issuedAt: Date.now(), + }); const startedAt = Date.now(); + let snapshot: ContainerSnapshot; try { - const snapshot = await withTimeout( + snapshot = await withTimeout( container.snapshotContainer({}), - REPO_CAPTURE_TIMEOUT_MS, + SNAPSHOT_CAPTURE_TIMEOUT_MS, 'container snapshot timed out' ); - const snapshotMs = Date.now() - startedAt; - const size = Number.isFinite(snapshot.size) ? snapshot.size : undefined; - if (!(await this.isRunningAllocation(allocationRef))) { - logRepositoryCapture('abandoned', snapshotMs, { size }); - return false; - } - const key = await repoSnapshotIndexKey(repoKey, record.startImage); - // The capture start bounds the contents' age from above, so an age logged on - // use never understates how stale the repository is. - const stored = await index.store(key, { - snapshotId: snapshot.id, - capturedAt: startedAt, - ...(size === undefined ? {} : { size }), - ...(commit === undefined ? {} : { commit }), - }); - logRepositoryCapture(stored ? 'stored' : 'index_unavailable', snapshotMs, { size }); - return stored; } catch (error) { - logRepositoryCapture('failed', Date.now() - startedAt, { error }); - return false; + logSnapshotCapture('snapshot_failed', Date.now() - startedAt, { error }); + return this.storeCapture(captureId, key, { + state: 'failed', + allocationRef, + reason: 'snapshot_failed', + }); + } + const durationMs = Date.now() - startedAt; + const size = Number.isFinite(snapshot.size) ? snapshot.size : undefined; + if (!(await this.isRunningAllocation(allocationRef))) { + logSnapshotCapture('abandoned', durationMs, { size }); + return this.storeCapture(captureId, key, { + state: 'failed', + allocationRef, + reason: 'abandoned', + }); } + logSnapshotCapture('stored', durationMs, { size }); + // The capture start bounds the contents' age from above, so an age shown on + // use never understates how stale the snapshot is. + return this.storeCapture(captureId, key, { + state: 'stored', + allocationRef, + snapshotId: snapshot.id, + ...(size === undefined ? {} : { size }), + capturedAt: startedAt, + baseImage, + durationMs, + }); + } + + private async storeCapture( + captureId: string, + key: string, + record: CaptureRecord + ): Promise { + const { index, evicted } = retainCaptureIds( + parseCaptureIndex(await this.ctx.storage.get(CAPTURE_INDEX_KEY)), + captureId + ); + await this.ctx.storage.put({ [key]: record, [CAPTURE_INDEX_KEY]: index }); + if (evicted.length > 0) await this.ctx.storage.delete(evicted.map(id => captureKey(id))); + return captureResult(record, record.state === 'issued'); } private async isRunningAllocation(allocationRef: string): Promise { diff --git a/services/cloud-agent-next/src/sandbox-containers/main-process.ts b/services/cloud-agent-next/src/sandbox-containers/main-process.ts index 7d6b44d1a1a..e1c8f151559 100644 --- a/services/cloud-agent-next/src/sandbox-containers/main-process.ts +++ b/services/cloud-agent-next/src/sandbox-containers/main-process.ts @@ -1,8 +1,20 @@ -import { CONTROL_SUPERVISOR_PATH } from '../sandbox-control/container-paths.js'; +import { + CONTROL_SUPERVISOR_PATH, + ENVIRONMENT_BUILDER_PATH, +} from '../sandbox-control/container-paths.js'; import { diagnosticCause, type ControlDiagnosticFields } from '../sandbox-control/diagnostics.js'; import type { ProviderStartupCommand } from '../sandbox-control/provider.js'; +import { RUNTIME_BUN_PATH } from '../shared/runtime-distribution.js'; -export type MainProcessIdentity = 'sleep' | 'supervisor' | 'startup' | 'ambiguous'; +export type MainProcessIdentity = + | 'sleep' + | 'supervisor' + | 'environment-builder' + | 'startup' + | 'ambiguous'; + +/** The main processes a native start can issue; the image default `sleep` is not one. */ +export type NativeMainProcess = Extract; /** * One read of `/proc/1/cmdline` and the identity it decided. Everything except a @@ -19,6 +31,15 @@ const MAIN_PROCESS_SLEEP_CMDLINE = new TextEncoder().encode('sleep\0infinity'); const MAIN_PROCESS_SUPERVISOR_CMDLINE = new TextEncoder().encode( `/bin/sh\0${CONTROL_SUPERVISOR_PATH}` ); +const MAIN_PROCESS_BUILDER_CMDLINE = new TextEncoder().encode( + `${RUNTIME_BUN_PATH}\0${ENVIRONMENT_BUILDER_PATH}` +); + +/** The argv a native start issues for each main process, matching its identity bytes. */ +export const NATIVE_MAIN_PROCESS_ARGV: Record = { + supervisor: ['/bin/sh', CONTROL_SUPERVISOR_PATH], + 'environment-builder': [RUNTIME_BUN_PATH, ENVIRONMENT_BUILDER_PATH], +}; function bytesEqual(left: Uint8Array, right: Uint8Array): boolean { if (left.byteLength !== right.byteLength) return false; @@ -35,9 +56,11 @@ function withoutTrailingNul(cmdline: Uint8Array): Uint8Array { } /** - * Classify PID 1 from its exact argv bytes, allowing a trailing NUL. The supplied - * startup command and stable supervisor confirm issuance. The image default - * `sleep infinity` leaves the existing exec path available. + * Classify PID 1 from its `/proc/1/cmdline` bytes. The known main processes are + * the image default `sleep infinity`, the native supervisor entrypoint and the + * environment builder; a trailing NUL is tolerated. Timeout, non-zero, empty, + * and anything else (including a runtime installer that has not handed off + * yet) is ambiguous and must fail the launch rather than guess. */ export function classifyMainProcess( cmdline: Uint8Array, @@ -45,6 +68,7 @@ export function classifyMainProcess( ): MainProcessIdentity { const trimmed = withoutTrailingNul(cmdline); if (bytesEqual(trimmed, MAIN_PROCESS_SUPERVISOR_CMDLINE)) return 'supervisor'; + if (bytesEqual(trimmed, MAIN_PROCESS_BUILDER_CMDLINE)) return 'environment-builder'; if (command && bytesEqual(trimmed, new TextEncoder().encode(command.argv.join('\0')))) return 'startup'; if (bytesEqual(trimmed, MAIN_PROCESS_SLEEP_CMDLINE)) return 'sleep'; diff --git a/services/cloud-agent-next/src/sandbox-containers/native-capture.ts b/services/cloud-agent-next/src/sandbox-containers/native-capture.ts new file mode 100644 index 00000000000..942f162eb83 --- /dev/null +++ b/services/cloud-agent-next/src/sandbox-containers/native-capture.ts @@ -0,0 +1,114 @@ +import { z } from 'zod'; + +/** + * Durable capture records (plan "Build admission and execution", steps 8 and 10). + * + * A capture is requested with a caller-chosen capture ID. The container DO stores + * `issued` before it asks the platform for a snapshot and the outcome before it + * replies, so a caller that lost the reply (a deploy restarts both DOs) reads the + * result back by ID. An `issued` record whose snapshot call is not running in + * this isolate any more is `uncertain`: the platform may or may not have taken a + * snapshot, and the caller must fail that attempt rather than snapshot again. + * Capture never publishes anything; only the environment coordinator does. + */ + +export const CAPTURE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/; + +/** Captures retained per container DO, newest first. Older records are deleted. */ +export const MAX_RETAINED_CAPTURES = 8; + +const CAPTURE_KEY_PREFIX = 'containers:capture:v1:'; +export const CAPTURE_INDEX_KEY = 'containers:captures:v1'; + +const captureRecordSchema = z.discriminatedUnion('state', [ + z + .object({ + state: z.literal('issued'), + allocationRef: z.string().min(1), + issuedAt: z.number().int().nonnegative(), + }) + .strict(), + z + .object({ + state: z.literal('stored'), + allocationRef: z.string().min(1), + snapshotId: z.string().min(1), + size: z.number().nonnegative().optional(), + capturedAt: z.number().int().nonnegative(), + baseImage: z.string().min(1), + durationMs: z.number().int().nonnegative(), + }) + .strict(), + z + .object({ + state: z.literal('failed'), + allocationRef: z.string().min(1), + reason: z.enum(['not_running', 'abandoned', 'snapshot_failed', 'uncertain']), + }) + .strict(), +]); + +export type CaptureRecord = z.infer; + +/** What a capture caller learns. `in_progress` only while this isolate still awaits it. */ +export type NativeCaptureResult = + | { + status: 'stored'; + snapshotId: string; + size?: number; + capturedAt: number; + baseImage: string; + durationMs: number; + } + | { status: 'failed'; reason: Extract['reason'] } + | { status: 'in_progress' }; + +export function captureKey(captureId: string): string { + if (!CAPTURE_ID_PATTERN.test(captureId)) throw new Error('Invalid capture id'); + return `${CAPTURE_KEY_PREFIX}${captureId}`; +} + +export function parseCaptureRecord(value: unknown): CaptureRecord | null { + const parsed = captureRecordSchema.safeParse(value); + return parsed.success ? parsed.data : null; +} + +/** + * The caller-facing result of a stored record. `issued` is decided by whether the + * snapshot call is still in flight in this isolate: if not, nobody can learn its + * outcome any more. + */ +export function captureResult(record: CaptureRecord, inFlight: boolean): NativeCaptureResult { + switch (record.state) { + case 'stored': + return { + status: 'stored', + snapshotId: record.snapshotId, + ...(record.size === undefined ? {} : { size: record.size }), + capturedAt: record.capturedAt, + baseImage: record.baseImage, + durationMs: record.durationMs, + }; + case 'failed': + return { status: 'failed', reason: record.reason }; + case 'issued': + return inFlight ? { status: 'in_progress' } : { status: 'failed', reason: 'uncertain' }; + } +} + +/** The capture index with `captureId` first and the IDs to delete past the retention bound. */ +export function retainCaptureIds( + index: readonly string[], + captureId: string +): { index: string[]; evicted: string[] } { + const next = [captureId, ...index.filter(id => id !== captureId)]; + return { + index: next.slice(0, MAX_RETAINED_CAPTURES), + evicted: next.slice(MAX_RETAINED_CAPTURES), + }; +} + +export function parseCaptureIndex(value: unknown): string[] { + const parsed = z.array(z.string().regex(CAPTURE_ID_PATTERN)).safeParse(value); + return parsed.success ? parsed.data : []; +} diff --git a/services/cloud-agent-next/src/sandbox-containers/native-start.test.ts b/services/cloud-agent-next/src/sandbox-containers/native-start.test.ts new file mode 100644 index 00000000000..c1bf100b2c8 --- /dev/null +++ b/services/cloud-agent-next/src/sandbox-containers/native-start.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest'; +import { CONTROL_SUPERVISOR_PATH } from '../sandbox-control/container-paths.js'; +import { planNativeStart } from './native-start.js'; +import { captureResult, retainCaptureIds, MAX_RETAINED_CAPTURES } from './native-capture.js'; + +const REQUEST = { + image: 'registry.example/app:1', + instance: 'standard-3' as const, + mainProcess: 'supervisor' as const, + command: { argv: ['/bin/sh', CONTROL_SUPERVISOR_PATH] }, + env: { A: '1' }, + interception: false, +}; + +describe('planNativeStart', () => { + it('uses a snapshot only when allowed and captured from the same image', () => { + const snapshot = { snapshotId: 'snap-1', baseImage: 'registry.example/app:1' }; + expect(planNativeStart({ ...REQUEST, snapshot }, true)).toMatchObject({ + source: 'repository', + snapshotId: 'snap-1', + reason: 'snapshot', + options: { containerSnapshot: { id: 'snap-1' } }, + }); + expect(planNativeStart({ ...REQUEST, snapshot }, false).reason).toBe('not_allowed'); + expect( + planNativeStart({ ...REQUEST, snapshot: { ...snapshot, baseImage: 'other' } }, true) + ).toMatchObject({ + source: 'image', + reason: 'image_mismatch', + options: { image: REQUEST.image }, + }); + expect(planNativeStart(REQUEST, true).reason).toBe('no_snapshot'); + }); + + it('preserves the supplied runtime command and adds interception without dropping caller env', () => { + const plan = planNativeStart( + { + ...REQUEST, + interception: true, + egressAllowlist: ['example.com'], + command: { argv: ['/bin/sh', '-c', 'download-and-exec'] }, + }, + true + ); + expect(plan.options.enableInternet).toBe(false); + expect(plan.options.entrypoint).toEqual(['/bin/sh', '-c', 'download-and-exec']); + expect(plan.options.env).toMatchObject({ + A: '1', + SANDBOX_INTERCEPT_HTTPS: '1', + CONTROL_PLANE_NATIVE_LOGS: '1', + }); + }); +}); + +describe('native capture records', () => { + it('maps an issued record to in progress only while its call runs', () => { + const issued = { state: 'issued' as const, allocationRef: 'a', issuedAt: 1 }; + expect(captureResult(issued, true)).toEqual({ status: 'in_progress' }); + expect(captureResult(issued, false)).toEqual({ status: 'failed', reason: 'uncertain' }); + }); + + it('keeps the newest captures and evicts the rest', () => { + const index = Array.from({ length: MAX_RETAINED_CAPTURES }, (_, i) => `c${i}`); + expect(retainCaptureIds(index, 'new')).toEqual({ + index: ['new', ...index.slice(0, MAX_RETAINED_CAPTURES - 1)], + evicted: [`c${MAX_RETAINED_CAPTURES - 1}`], + }); + expect(retainCaptureIds(['a', 'b'], 'b')).toEqual({ index: ['b', 'a'], evicted: [] }); + }); +}); diff --git a/services/cloud-agent-next/src/sandbox-containers/native-start.ts b/services/cloud-agent-next/src/sandbox-containers/native-start.ts new file mode 100644 index 00000000000..ce30715dd36 --- /dev/null +++ b/services/cloud-agent-next/src/sandbox-containers/native-start.ts @@ -0,0 +1,122 @@ +import { + CONTAINERS_INTERCEPT_CA_PATH, + SANDBOX_INTERCEPT_HTTPS_ENABLED, + SANDBOX_INTERCEPT_HTTPS_ENV, +} from '../shared/container-intercept.js'; +import { CONTROL_PLANE_NATIVE_LOGS_ENV } from '../shared/control-diagnostics.js'; +import type { ProviderStartupCommand } from '../sandbox-control/provider.js'; +import { type NativeMainProcess } from './main-process.js'; + +import { containerInstanceStartValue, type ContainerInstanceSize } from './instance-size.js'; + +export type { ContainerInstanceSize } from './instance-size.js'; + +/** What a physical start used: the image, or an environment snapshot of it. */ +export type ContainersStartSource = 'image' | 'repository'; + +/** + * A published environment snapshot selected by the environment coordinator. + * `baseImage` is the image the builder ran; a start whose image differs is a + * fresh start, because the snapshot's base layers belong to another image. + */ +export type NativeStartSnapshot = { + snapshotId: string; + baseImage: string; +}; + +export type NativeStartRequest = { + image: string; + instance: ContainerInstanceSize; + mainProcess: NativeMainProcess; + env: Record; + /** Outbound interception is installed: the main process must trust the intercept CA. */ + interception: boolean; + egressAllowlist?: string[]; + command: ProviderStartupCommand; + snapshot?: NativeStartSnapshot; +}; + +/** Why a start uses the image even though one may have been offered. */ +export type NativeStartReason = + | 'no_snapshot' + | 'not_allowed' + | 'image_mismatch' + | 'snapshot' + | 'restore_failures'; + +export type NativeStartPlan = { + image: string; + source: ContainersStartSource; + options: ContainerStartupOptions; + snapshotId: string | null; + reason: NativeStartReason; +}; + +function interceptedProcessEnv(env: Record): Record { + // Bun reads NODE_EXTRA_CA_CERTS only at process start, so the injected CA file must be + // readable before this exec for the wrapper's own TLS; cert.ts only completes the bundle + // append and the child env afterwards. + return { + ...env, + [SANDBOX_INTERCEPT_HTTPS_ENV]: SANDBOX_INTERCEPT_HTTPS_ENABLED, + NODE_EXTRA_CA_CERTS: CONTAINERS_INTERCEPT_CA_PATH, + }; +} + +/** + * The env for a native start and for a sleep-main-process exec. Interception CA + * goes on both; the stderr gate is native-only and set here, never in the + * shared launch-env builder. + */ +export function nativeLaunchEnv( + env: Record, + interception: boolean +): Record { + return { + ...(interception ? interceptedProcessEnv(env) : env), + [CONTROL_PLANE_NATIVE_LOGS_ENV]: '1', + }; +} + +function startOptions(request: NativeStartRequest, snapshotId?: string): ContainerStartupOptions { + const env = nativeLaunchEnv(request.env, request.interception); + return { + ...(snapshotId === undefined + ? { image: request.image } + : { containerSnapshot: { id: snapshotId } }), + instance: containerInstanceStartValue(request.instance), + enableInternet: request.egressAllowlist === undefined, + entrypoint: [...request.command.argv], + env, + }; +} + +/** + * The one start-source rule: start from the offered environment snapshot when the + * caller allows it and it was captured from this image, and from the image + * otherwise. The snapshot handle never comes from a request: only the environment + * coordinator publishes it and the Sandbox DO forwards it. + */ +export function planNativeStart( + request: NativeStartRequest, + allowSnapshot: boolean +): NativeStartPlan { + const image = (reason: NativeStartReason): NativeStartPlan => ({ + image: request.image, + source: 'image', + options: startOptions(request), + snapshotId: null, + reason, + }); + const snapshot = request.snapshot; + if (snapshot === undefined) return image('no_snapshot'); + if (!allowSnapshot) return image('not_allowed'); + if (snapshot.baseImage !== request.image) return image('image_mismatch'); + return { + image: request.image, + source: 'repository', + options: startOptions(request, snapshot.snapshotId), + snapshotId: snapshot.snapshotId, + reason: 'snapshot', + }; +} diff --git a/services/cloud-agent-next/src/sandbox-containers/repo-snapshot-index.ts b/services/cloud-agent-next/src/sandbox-containers/repo-snapshot-index.ts deleted file mode 100644 index 4f10e9c34da..00000000000 --- a/services/cloud-agent-next/src/sandbox-containers/repo-snapshot-index.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { z } from 'zod'; -import { logger } from '../logger.js'; - -/** - * KV `expirationTtl` for one index entry: how long a snapshot nobody uses stays - * addressable, under the platform's 30-day snapshot lifetime. A snapshot in use is - * refreshed earlier by the wrapper, from the adopted workspace. - */ -export const REPO_SNAPSHOT_INDEX_TTL_SECONDS = 10 * 24 * 60 * 60; -export const REPO_SNAPSHOT_MAX_START_FAILURES = 10; - -const KEY_PREFIX = 'repo-snapshot:v1:'; - -// `capturedAt` (epoch ms) and `size` are diagnostics only; entries written before -// them lack both, so they stay optional. -const repoSnapshotEntrySchema = z - .object({ - snapshotId: z.string().min(1), - commit: z.string().min(1).optional(), - capturedAt: z.number().optional(), - size: z.number().optional(), - }) - .strict(); - -export type RepoSnapshotEntry = z.infer; - -/** - * The index key for one repository snapshot. `repoKey` is already a keyed hash - * over the scope and repository (Sandbox DO), so a digest that adds the image, - * which this DO owns, stays unguessable without this DO holding the key. A deploy - * that changes the image changes the key, which is how an image-tied snapshot is - * retired. - */ -export async function repoSnapshotIndexKey(repoKey: string, image: string): Promise { - const bytes = new TextEncoder().encode(JSON.stringify([repoKey, image])); - const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)); - return KEY_PREFIX + Array.from(digest, byte => byte.toString(16).padStart(2, '0')).join(''); -} - -/** - * The repository snapshot index. Every operation is best effort: an unavailable - * store is a miss, never a failed start, so a snapshot is an optimisation only. - */ -export type RepoSnapshotIndex = { - lookup(key: string): Promise; - store(key: string, entry: RepoSnapshotEntry): Promise; -}; - -export function createRepoSnapshotIndex(kv: KVNamespace | undefined): RepoSnapshotIndex | null { - if (kv === undefined) return null; - return { - async lookup(key) { - try { - const raw = await kv.get(key, 'json'); - if (raw === null) return null; - const parsed = repoSnapshotEntrySchema.safeParse(raw); - return parsed.success ? parsed.data : null; - } catch (error) { - warn('lookup', error); - return null; - } - }, - async store(key, entry) { - try { - await kv.put(key, JSON.stringify(entry), { - expirationTtl: REPO_SNAPSHOT_INDEX_TTL_SECONDS, - }); - return true; - } catch (error) { - warn('store', error); - return false; - } - }, - }; -} - -function warn(operation: string, error: unknown): void { - logger - .withFields({ - operation, - error: error instanceof Error ? error.message : 'unknown', - }) - .warn('Repository snapshot index unavailable'); -} diff --git a/services/cloud-agent-next/src/sandbox-containers/sandbox-containers.test.ts b/services/cloud-agent-next/src/sandbox-containers/sandbox-containers.test.ts index a6221549ba6..9672a92eebd 100644 --- a/services/cloud-agent-next/src/sandbox-containers/sandbox-containers.test.ts +++ b/services/cloud-agent-next/src/sandbox-containers/sandbox-containers.test.ts @@ -21,9 +21,10 @@ vi.mock('@cloudflare/sandbox', () => ({ Sandbox: class {} })); import { CONTROL_SUPERVISOR_PATH, CONTROL_WRAPPER_LOG_PATH, + ENVIRONMENT_BUILDER_PATH, } from '../sandbox-control/container-paths.js'; +import { RUNTIME_BUN_PATH } from '../shared/runtime-distribution.js'; import { logger } from '../logger.js'; -import { repoSnapshotIndexKey } from './repo-snapshot-index.js'; import { SandboxContainers, type ContainerInstanceSize, @@ -124,7 +125,11 @@ class FakeContainer { // PID 1 as seen through `/proc/1/cmdline`. A manual `running = true` models the // deployed `sleep infinity` main process; `start()` with the native entrypoint // models the supervisor main process. - mainProcess: 'sleep' | 'supervisor' | 'ambiguous' = 'sleep'; + mainProcess: 'sleep' | 'supervisor' | 'environment-builder' | 'ambiguous' = 'sleep'; + // A runtime installer start (`/bin/sh -c `): hand off to the entry point + // named in the launch, or exit with this code so `monitor()` rejects. + runtimeOutcome: { kind: 'handoff' } | { kind: 'exit'; code: number } = { kind: 'handoff' }; + private exitError: Error | undefined; // What native `start()` leaves as PID 1. A lucky start normally leaves the // supervisor; tests override this to model a start whose main process is wrong. identityAfterStart: 'sleep' | 'supervisor' | 'ambiguous' = 'supervisor'; @@ -143,8 +148,23 @@ class FakeContainer { this.startCalls.push(options as ContainerStartupOptions); if (this.startBehavior === 'reject') throw new Error('container start failed'); this.running = true; - if (options?.entrypoint?.includes(CONTROL_SUPERVISOR_PATH)) { + this.exitError = undefined; + const entrypoint = options?.entrypoint ?? []; + if (entrypoint.includes(CONTROL_SUPERVISOR_PATH)) { this.mainProcess = this.identityAfterStart; + } else if (entrypoint.includes(ENVIRONMENT_BUILDER_PATH)) { + this.mainProcess = 'environment-builder'; + } else if (entrypoint[1] === '-c') { + if (this.runtimeOutcome.kind === 'exit') { + this.running = false; + this.exitError = new Error( + `Container exited with unexpected exit code: ${this.runtimeOutcome.code}` + ); + } else { + this.mainProcess = entrypoint[2]?.includes('kilocode-environment-builder.js') + ? 'environment-builder' + : this.identityAfterStart; + } } if (this.stopAfterStart) this.running = false; if (this.startBehavior === 'effect-then-reject') { @@ -164,7 +184,9 @@ class FakeContainer { stdout: this.mainProcess === 'supervisor' ? `/bin/sh\0${CONTROL_SUPERVISOR_PATH}` - : 'sleep\0infinity', + : this.mainProcess === 'environment-builder' + ? `${RUNTIME_BUN_PATH}\0${ENVIRONMENT_BUILDER_PATH}` + : 'sleep\0infinity', }); } return this.execHandler(cmd); @@ -222,6 +244,7 @@ class FakeContainer { async monitor(): Promise { this.monitorCalls += 1; + if (this.exitError) throw this.exitError; if (!this.stopAfterStart) await new Promise(() => {}); } @@ -252,7 +275,13 @@ function setup( async get(key: string): Promise { return storage.map.get(key) as T | undefined; }, - async put(key: string, value: unknown): Promise { + async put(key: string | Record, value?: unknown): Promise { + if (typeof key !== 'string') { + for (const [entryKey, entryValue] of Object.entries(key)) { + await storage.put(entryKey, entryValue); + } + return; + } const decision = putGate?.(value) ?? 'pass'; if (decision === 'fail') { putGate = undefined; @@ -270,7 +299,8 @@ function setup( } storage.map.set(key, value); }, - async delete(key: string): Promise { + async delete(key: string | string[]): Promise { + if (Array.isArray(key)) return key.filter(entry => storage.map.delete(entry)).length; return storage.map.delete(key); }, async setAlarm(scheduledTime: number): Promise { @@ -336,18 +366,20 @@ describe('supplied startup command', () => { const command = { argv: ['/bin/sh', '-c', 'exec /opt/selected/start "two words"'] }; it.each(['image', 'repository'] as const)('starts the supplied command from %s', async source => { - const key = await indexKeyFor(); - const { kv } = fakeKv(source === 'repository' ? { [key]: { snapshotId: 'snap-repo' } } : {}); - const { instance, container, readRecord } = setup({ env: { REPO_SNAPSHOTS: kv } }); + const { instance, container, readRecord } = setup(); container.cmdlineRaw = `${command.argv.join('\0')}\0`; - await expect(launchFromRepo(instance, REF_A, { command })).resolves.toEqual({ + await expect( + source === 'repository' + ? launchFromSnapshot(instance, REF_A, { command }) + : instance.launchWrapper({ allocationRef: REF_A, command, env: {}, instance: 'standard-4' }) + ).resolves.toEqual({ started: true, startSource: source, }); expect(container.startCalls[0]?.entrypoint).toEqual(command.argv); if (source === 'repository') - expect(container.startCalls[0]?.containerSnapshot).toEqual({ id: 'snap-repo' }); + expect(container.startCalls[0]?.containerSnapshot).toEqual({ id: 'snap-env' }); expect(container.execCalls.map(call => call.cmd)).toEqual([['cat', '/proc/1/cmdline']]); expect(readRecord().state).toBe('running'); }); @@ -2074,7 +2106,7 @@ describe('SandboxContainers native start confirmation', () => { ); const failed = expect(launch(instance, REF_A)).rejects.toThrow( - 'stopped before the supervisor main process was confirmed' + 'stopped before the main process was confirmed' ); await vi.advanceTimersByTimeAsync(100); await failed; @@ -2090,7 +2122,7 @@ describe('SandboxContainers native start confirmation', () => { vi.spyOn(container, 'exec').mockRejectedValue(new Error('Container is not running')); await expect(launch(instance, REF_A)).rejects.toThrow( - 'stopped before the supervisor main process was confirmed' + 'stopped before the main process was confirmed' ); expect(diagnostics('supervisor_confirm')).toMatchObject([ { @@ -2508,102 +2540,49 @@ describe('SandboxContainers lease and log', () => { }); }); -type KvCall = { op: 'get' | 'put' | 'delete'; key: string; options?: unknown }; - -function fakeKv(initial: Record = {}) { - const data = new Map( - Object.entries(initial).map(([key, value]) => [key, JSON.stringify(value)]) - ); - const calls: KvCall[] = []; - const failing = { get: false, put: false }; - const kv = { - async get(key: string) { - calls.push({ op: 'get', key }); - if (failing.get) throw new Error('kv unavailable'); - const raw = data.get(key); - return raw === undefined ? null : JSON.parse(raw); - }, - async put(key: string, value: string, options?: unknown) { - calls.push({ op: 'put', key, options }); - if (failing.put) throw new Error('kv unavailable'); - data.set(key, value); - }, - async delete(key: string) { - calls.push({ op: 'delete', key }); - data.delete(key); - }, - }; - return { kv: kv as unknown as KVNamespace, data, calls, failing }; -} - const IMAGE = 'registry.example/kilo/app:test'; -const REPO_KEY = 'repo-key-1'; +const SNAPSHOT = { snapshotId: 'snap-env', baseImage: IMAGE }; -async function indexKeyFor(repoKey = REPO_KEY, image = IMAGE) { - return repoSnapshotIndexKey(repoKey, image); -} - -function launchFromRepo(instance: SandboxContainers, ref: string, extra = {}) { +function launchFromSnapshot(instance: SandboxContainers, ref: string, extra = {}) { return instance.launchWrapper({ command: preinstalledStartup(), allocationRef: ref, env: {}, instance: 'standard-4', - repoKey: REPO_KEY, + snapshot: SNAPSHOT, ...extra, }); } -async function launchWithImageFallback(instance: SandboxContainers, ref: string) { - const result = await launchFromRepo(instance, ref); - if (!result.snapshotFailed) return result; - expect(result).toEqual({ started: false, startSource: 'repository', snapshotFailed: true }); - await expect(instance.stop(ref)).resolves.toBe('terminal'); - return launchFromRepo(instance, ref, { skipRepository: true }); -} - -describe('SandboxContainers repository snapshots', () => { - it('restricts repository restore and subsequent image fallback with the same policy', async () => { - vi.useFakeTimers(); - const key = await indexKeyFor(); - const { kv } = fakeKv({ [key]: { snapshotId: 'snap-repo' } }); - const { instance, container, readRecord } = setup({ env: { REPO_SNAPSHOTS: kv } }); - const outbound = attachOutbound(instance); - container.identityAfterStart = 'ambiguous'; - const policy = { egressAllowlist: ['example.com'] }; - const pending = launchFromRepo(instance, REF_A, policy); - await vi.waitFor(() => expect(container.startCalls).toHaveLength(1)); - await vi.advanceTimersByTimeAsync(300_000); - const first = await pending; - expect(first.snapshotFailed).toBe(true); - expect(readRecord().egressPolicy?.allowlist).toEqual(['example.com']); - await expect(instance.stop(REF_A)).resolves.toBe('terminal'); - container.identityAfterStart = 'supervisor'; - await launchFromRepo(instance, REF_A, { ...policy, skipRepository: true }); - - expect(container.startCalls.map(options => options.enableInternet)).toEqual([false, false]); - expect(container.startCalls[0]).toMatchObject({ containerSnapshot: { id: 'snap-repo' } }); - expect(container.startCalls[1]).toMatchObject({ image: IMAGE }); +describe('SandboxContainers environment snapshot starts', () => { + it('stops retrying a repeatedly failing snapshot but permits a replacement', async () => { + const { instance, container } = setup(); + for (let attempt = 0; attempt < 3; attempt += 1) { + container.startBehavior = 'effect-then-reject'; + expect(await launchFromSnapshot(instance, REF_A)).toMatchObject({ snapshotFailed: true }); + await instance.stop(REF_A); + } + container.startBehavior = 'ok'; + expect(await launchFromSnapshot(instance, REF_A)).toMatchObject({ startSource: 'image' }); + expect(container.startCalls.at(-1)).toHaveProperty('image', IMAGE); + await instance.stop(REF_A); expect( - container.startCalls.every(options => options.env?.SANDBOX_INTERCEPT_HTTPS === '1') - ).toBe(true); - expect(outbound.mock.calls.map(call => call[0].props.egressAllowlist)).toEqual([ - ['example.com'], - ['example.com'], - ]); + await launchFromSnapshot(instance, REF_A, { + snapshot: { ...SNAPSHOT, snapshotId: 'replacement' }, + }) + ).toMatchObject({ startSource: 'repository' }); }); - it('starts from the repository snapshot stored for the repo key and the current image', async () => { - const key = await indexKeyFor(); - const { kv } = fakeKv({ [key]: { snapshotId: 'snap-repo', commit: 'abc123' } }); - const { instance, container, readRecord } = setup({ env: { REPO_SNAPSHOTS: kv } }); + it('starts from the offered snapshot captured from the current image', async () => { + const { diagnostics } = captureLogs(); + const { instance, container, readRecord } = setup(); - const result = await launchFromRepo(instance, REF_A); + const result = await launchFromSnapshot(instance, REF_A); expect(result).toEqual({ started: true, startSource: 'repository' }); const options = container.startCalls[0] as Record; expect(options).toEqual({ - containerSnapshot: { id: 'snap-repo' }, + containerSnapshot: { id: 'snap-env' }, instance: 'standard-4', enableInternet: true, entrypoint: ['/bin/sh', CONTROL_SUPERVISOR_PATH], @@ -2611,242 +2590,72 @@ describe('SandboxContainers repository snapshots', () => { }); expect('image' in options).toBe(false); expect(readRecord()).toMatchObject({ state: 'running', startSource: 'repository' }); - await expect(launchFromRepo(instance, REF_A)).resolves.toEqual({ + expect(diagnostics('snapshot_lookup')[0].fields).toMatchObject({ + source: 'repository', + reason: 'snapshot', + }); + await expect(launchFromSnapshot(instance, REF_A)).resolves.toEqual({ started: false, startSource: 'repository', }); }); - it('captures under the start image after the configured image changes and the DO reconstructs', async () => { - const { kv, data } = fakeKv(); - const initial = setup({ env: { REPO_SNAPSHOTS: kv } }); - await launchFromRepo(initial.instance, REF_A); - const restored = setup({ - record: initial.readRecord(), - running: true, - env: { REPO_SNAPSHOTS: kv }, - }); - const newImage = 'registry.example/kilo/app:next'; - restored.container.images.app = newImage; - - await expect(restored.instance.captureRepository(REF_A, REPO_KEY, 'abc123')).resolves.toBe( - true - ); - - expect(data.has(await indexKeyFor(REPO_KEY, newImage))).toBe(false); - expect(JSON.parse(data.get(await indexKeyFor()) ?? 'null')).toMatchObject({ - snapshotId: 'snap-1', - commit: 'abc123', - }); - }); - - it('starts from the image when the index has no entry, and a different image has none', async () => { - const other = await indexKeyFor(REPO_KEY, 'registry.example/kilo/app:previous'); - const { kv } = fakeKv({ [other]: { snapshotId: 'snap-old-image' } }); - const { instance, container } = setup({ env: { REPO_SNAPSHOTS: kv } }); - - await expect(launchFromRepo(instance, REF_A)).resolves.toEqual({ - started: true, - startSource: 'image', - }); - expect(container.startCalls[0]).toEqual(nativeStartOptions('standard-4')); - }); - - it('never consults the index without a repo key or a KV binding', async () => { - const key = await indexKeyFor(); - const withoutKey = fakeKv({ [key]: { snapshotId: 'snap-repo' } }); - const first = setup({ env: { REPO_SNAPSHOTS: withoutKey.kv } }); - await expect(launch(first.instance, REF_A)).resolves.toMatchObject({ startSource: 'image' }); - expect(withoutKey.calls).toEqual([]); + it('starts from the image when the snapshot belongs to another image or none is offered', async () => { + const { diagnostics } = captureLogs(); + const first = setup(); + await expect( + launchFromSnapshot(first.instance, REF_A, { + snapshot: { snapshotId: 'snap-old', baseImage: 'registry.example/kilo/app:previous' }, + }) + ).resolves.toEqual({ started: true, startSource: 'image' }); + expect(first.container.startCalls[0]).toEqual(nativeStartOptions('standard-4')); const second = setup(); - await expect(launchFromRepo(second.instance, REF_A)).resolves.toMatchObject({ - startSource: 'image', - }); - }); - - it('treats an unavailable or malformed index entry as a miss', async () => { - const key = await indexKeyFor(); - const broken = fakeKv(); - broken.failing.get = true; - const first = setup({ env: { REPO_SNAPSHOTS: broken.kv } }); - await expect(launchFromRepo(first.instance, REF_A)).resolves.toMatchObject({ - startSource: 'image', - }); - - const malformed = fakeKv({ [key]: { snapshotId: 42 } }); - const second = setup({ env: { REPO_SNAPSHOTS: malformed.kv } }); - await expect(launchFromRepo(second.instance, REF_A)).resolves.toMatchObject({ + await expect(launch(second.instance, REF_A)).resolves.toEqual({ + started: true, startSource: 'image', }); - }); - - it('logs the age and size of the snapshot it starts from, and no age for an older entry', async () => { - vi.useFakeTimers(); - vi.setSystemTime(10_000_000); - const { diagnostics } = captureLogs(); - const key = await indexKeyFor(); - const captured = fakeKv({ - [key]: { snapshotId: 'snap-repo', capturedAt: 10_000_000 - 90_000, size: SNAPSHOT_SIZE }, - }); - const first = setup({ env: { REPO_SNAPSHOTS: captured.kv } }); - await expect(launchFromRepo(first.instance, REF_A)).resolves.toMatchObject({ - startSource: 'repository', - }); - const older = fakeKv({ [key]: { snapshotId: 'snap-repo' } }); - const second = setup({ env: { REPO_SNAPSHOTS: older.kv } }); - await expect(launchFromRepo(second.instance, REF_A)).resolves.toMatchObject({ - startSource: 'repository', - }); - - const lookups = diagnostics('snapshot_lookup').map(line => line.fields); - expect(lookups).toEqual([ - expect.objectContaining({ - source: 'repository', - reason: 'index_hit', - ageMs: 90_000, - size: SNAPSHOT_SIZE, - }), - expect.objectContaining({ source: 'repository', reason: 'index_hit' }), - ]); - expect(lookups[1]).not.toHaveProperty('ageMs'); - expect(lookups[1]).not.toHaveProperty('size'); - expect(diagnostics('supervisor_confirm').map(line => line.fields.source)).toEqual([ - 'repository', - 'repository', + expect(diagnostics('snapshot_lookup').map(line => line.fields.reason)).toEqual([ + 'image_mismatch', + 'no_snapshot', ]); }); - it.each(['reject', 'effect-then-reject'] as const)( - 'falls back after a %s snapshot failure and retries the snapshot on ten separate starts', - async startBehavior => { - vi.useFakeTimers(); - vi.setSystemTime(10_000_000); - const key = await indexKeyFor(); - const entry = { - snapshotId: 'snap-missing', - commit: 'abc123', - capturedAt: 9_000_000, - size: SNAPSHOT_SIZE, - }; - const store = fakeKv({ [key]: entry }); - const { instance, container, readRecord } = setup({ env: { REPO_SNAPSHOTS: store.kv } }); - const start = container.start.bind(container); - vi.spyOn(container, 'start').mockImplementation(options => { - container.startBehavior = options?.containerSnapshot === undefined ? 'ok' : startBehavior; - start(options); - }); - - for (let attempt = 0; attempt < 10; attempt += 1) { - await expect(launchWithImageFallback(instance, `ref-${attempt}`)).resolves.toEqual({ - started: true, - startSource: 'image', - }); - expect(container.startCalls[attempt * 2]).toMatchObject({ - containerSnapshot: { id: 'snap-missing' }, - }); - expect(container.startCalls[attempt * 2 + 1]).toEqual(nativeStartOptions('standard-4')); - expect(readRecord().repositoryStartFailures).toEqual({ - snapshotId: 'snap-missing', - attempts: attempt + 1, - }); - } - expect(JSON.parse(store.data.get(key) ?? 'null')).toEqual(entry); - expect(store.calls.filter(call => call.op === 'put' || call.op === 'delete')).toEqual([]); - - await expect(launchFromRepo(instance, REF_B)).resolves.toEqual({ - started: true, - startSource: 'image', - }); - expect(container.startCalls).toHaveLength(21); - expect(container.startCalls[20]).toEqual(nativeStartOptions('standard-4')); - expect(readRecord()).toMatchObject({ - allocationRef: REF_B, - state: 'running', - startSource: 'image', - }); - expect(store.data.has(key)).toBe(true); - } - ); - - it('falls back when the snapshot supervisor cannot start and retries the snapshot on the next start', async () => { + it('applies the same egress policy to a snapshot start and its image retry', async () => { vi.useFakeTimers(); - const key = await indexKeyFor(); - const store = fakeKv({ [key]: { snapshotId: 'snap-broken' } }); - const { instance, container, readRecord } = setup({ env: { REPO_SNAPSHOTS: store.kv } }); - const start = container.start.bind(container); - const startSpy = vi.spyOn(container, 'start').mockImplementation(options => { - container.identityAfterStart = - options?.containerSnapshot === undefined ? 'supervisor' : 'ambiguous'; - start(options); - }); - const launched = expect(launchWithImageFallback(instance, REF_A)).resolves.toEqual({ - started: true, - startSource: 'image', - }); + const { instance, container, readRecord } = setup(); + const outbound = attachOutbound(instance); + container.identityAfterStart = 'ambiguous'; + const policy = { egressAllowlist: ['example.com'] }; + const pending = launchFromSnapshot(instance, REF_A, policy); await vi.waitFor(() => expect(container.startCalls).toHaveLength(1)); await vi.advanceTimersByTimeAsync(300_000); - await launched; - expect(JSON.parse(store.data.get(key) ?? 'null')).toEqual({ snapshotId: 'snap-broken' }); - expect(readRecord().repositoryStartFailures).toEqual({ - snapshotId: 'snap-broken', - attempts: 1, - }); - expect(store.calls.filter(call => call.op === 'delete')).toEqual([]); - - startSpy.mockRestore(); + expect((await pending).snapshotFailed).toBe(true); + expect(readRecord().egressPolicy?.allowlist).toEqual(['example.com']); + await expect(instance.stop(REF_A)).resolves.toBe('terminal'); container.identityAfterStart = 'supervisor'; - await expect(launchFromRepo(instance, REF_B)).resolves.toEqual({ - started: true, - startSource: 'repository', - }); - expect(container.startCalls[1]).toEqual(nativeStartOptions('standard-4')); - expect(container.startCalls[2]).toMatchObject({ containerSnapshot: { id: 'snap-broken' } }); - expect(readRecord().repositoryStartFailures).toEqual({ - snapshotId: 'snap-broken', - attempts: 1, - }); - }); - - it('marks nothing when an image start fails', async () => { - const store = fakeKv(); - const { instance, container } = setup({ env: { REPO_SNAPSHOTS: store.kv } }); - container.startBehavior = 'reject'; - - await expect(launchWithImageFallback(instance, REF_A)).rejects.toThrow( - 'container start failed' - ); - expect(store.calls.filter(call => call.op === 'delete')).toEqual([]); - expect(store.calls.filter(call => call.op === 'put')).toEqual([]); - }); - - it('propagates an image fallback failure without retrying either start', async () => { - const key = await indexKeyFor(); - const store = fakeKv({ [key]: { snapshotId: 'snap-bad' } }); - const { instance, container, readRecord } = setup({ env: { REPO_SNAPSHOTS: store.kv } }); - container.startBehavior = 'reject'; - - await expect(launchWithImageFallback(instance, REF_A)).rejects.toThrow( - 'container start failed' - ); - expect(container.startCalls).toHaveLength(2); - expect(container.startCalls[1]).toEqual(nativeStartOptions('standard-4')); - expect(readRecord()).toMatchObject({ - state: 'launching', + await instance.launchWrapper({ + command: preinstalledStartup(), allocationRef: REF_A, - startSource: 'image', - repositoryStartFailures: { snapshotId: 'snap-bad', attempts: 1 }, + env: {}, + instance: 'standard-4', + ...policy, }); - expect(store.calls.filter(call => call.op === 'put' || call.op === 'delete')).toEqual([]); + + expect(container.startCalls.map(options => options.enableInternet)).toEqual([false, false]); + expect(container.startCalls[0]).toMatchObject({ containerSnapshot: { id: 'snap-env' } }); + expect(container.startCalls[1]).toMatchObject({ image: IMAGE }); + expect(outbound.mock.calls.map(call => call[0].props.egressAllowlist)).toEqual([ + ['example.com'], + ['example.com'], + ]); }); - it('returns snapshot failure without destroying a container or issuing the image start', async () => { - const key = await indexKeyFor(); - const store = fakeKv({ [key]: { snapshotId: 'snap-bad' } }); - const { instance, container, readRecord } = setup({ env: { REPO_SNAPSHOTS: store.kv } }); + it('reports a failed snapshot start without destroying the container or counting it', async () => { + const { instance, container, readRecord } = setup(); container.startBehavior = 'effect-then-reject'; - await expect(launchFromRepo(instance, REF_A)).resolves.toEqual({ + await expect(launchFromSnapshot(instance, REF_A)).resolves.toEqual({ started: false, startSource: 'repository', snapshotFailed: true, @@ -2858,32 +2667,15 @@ describe('SandboxContainers repository snapshots', () => { allocationRef: REF_A, wrapperAttempt: 'exec_pending', }); - expect(store.data.has(key)).toBe(true); + expect(readRecord()).not.toHaveProperty('repositoryStartFailures'); }); - it.each([undefined, 310_000])( - 'allows a cold snapshot to become ready after four minutes with deadline %s', - async deadlineAt => { - vi.useFakeTimers(); - vi.setSystemTime(10_000); - const key = await indexKeyFor(); - const store = fakeKv({ [key]: { snapshotId: 'snap-cold' } }); - const { instance, container, readRecord } = setup({ env: { REPO_SNAPSHOTS: store.kv } }); - container.identityAfterStart = 'ambiguous'; + it('propagates an image start failure instead of reporting a snapshot failure', async () => { + const { instance, container } = setup(); + container.startBehavior = 'reject'; - const launched = expect(launchFromRepo(instance, REF_A, { deadlineAt })).resolves.toEqual({ - started: true, - startSource: 'repository', - }); - await vi.waitFor(() => expect(container.startCalls).toHaveLength(1)); - await vi.advanceTimersByTimeAsync(240_000); - container.mainProcess = 'supervisor'; - await vi.advanceTimersByTimeAsync(1_000); - await launched; - expect(container.startCalls).toHaveLength(1); - expect(readRecord().repositoryStartFailures).toBeUndefined(); - } - ); + await expect(launch(instance, REF_A)).rejects.toThrow('container start failed'); + }); it.each(['repository', 'image'] as const)( 'bounds %s confirmation by its start-source timeout', @@ -2891,15 +2683,14 @@ describe('SandboxContainers repository snapshots', () => { vi.useFakeTimers(); vi.setSystemTime(10_000); const { diagnostics } = captureLogs(); - const key = await indexKeyFor(); - const store = fakeKv(source === 'repository' ? { [key]: { snapshotId: 'snap-cold' } } : {}); - const { instance, container } = setup({ env: { REPO_SNAPSHOTS: store.kv } }); + const { instance, container } = setup(); container.identityAfterStart = 'ambiguous'; - const launch = launchFromRepo(instance, REF_A); + const started = + source === 'repository' ? launchFromSnapshot(instance, REF_A) : launch(instance, REF_A); const failed = source === 'repository' - ? expect(launch).resolves.toMatchObject({ snapshotFailed: true }) - : expect(launch).rejects.toThrow('wrapper exec timed out'); + ? expect(started).resolves.toMatchObject({ snapshotFailed: true }) + : expect(started).rejects.toThrow('wrapper exec timed out'); await vi.waitFor(() => expect(container.startCalls).toHaveLength(1)); const budget = source === 'repository' ? 300_000 : 90_000; await vi.advanceTimersByTimeAsync(budget - 1_000); @@ -2914,13 +2705,11 @@ describe('SandboxContainers repository snapshots', () => { vi.useFakeTimers(); vi.setSystemTime(10_000); const { diagnostics } = captureLogs(); - const key = await indexKeyFor(); - const store = fakeKv({ [key]: { snapshotId: 'snap-bad' } }); - const { instance, container } = setup({ env: { REPO_SNAPSHOTS: store.kv } }); + const { instance, container } = setup(); container.identityAfterStart = 'ambiguous'; const failed = expect( - launchFromRepo(instance, REF_A, { deadlineAt: 15_000 }) + launchFromSnapshot(instance, REF_A, { deadlineAt: 15_000 }) ).resolves.toMatchObject({ snapshotFailed: true }); await vi.waitFor(() => expect(container.startCalls).toHaveLength(1)); await vi.advanceTimersByTimeAsync(5_000); @@ -2936,312 +2725,399 @@ describe('SandboxContainers repository snapshots', () => { const { instance, container } = setup(); await expect( - launchFromRepo(instance, REF_A, { deadlineAt: 10_000, skipRepository: true }) + instance.launchWrapper({ + command: preinstalledStartup(), + allocationRef: REF_A, + env: {}, + instance: 'standard-4', + deadlineAt: 10_000, + }) ).rejects.toThrow('wrapper exec timed out'); expect(container.startCalls).toEqual([]); }); - it.each(['index_miss', 'retries_exhausted'] as const)( - 'keeps the full image readiness budget after %s', - async reason => { - vi.useFakeTimers(); - vi.setSystemTime(10_000); - const key = await indexKeyFor(); - const store = fakeKv(reason === 'index_miss' ? {} : { [key]: { snapshotId: 'snap-bad' } }); - const { instance, container } = setup({ - env: { REPO_SNAPSHOTS: store.kv }, - record: { - ...idleRecord, - repositoryStartFailures: { snapshotId: 'snap-bad', attempts: 10 }, - }, - }); - container.identityAfterStart = 'ambiguous'; + it('uses the recorded source when it resumes a launch that never started the wrapper', async () => { + const { instance, container, readRecord } = setup({ + record: { + ...idleRecord, + state: 'launching', + allocationRef: REF_A, + instance: 'standard-4', + startSource: 'repository', + wrapperAttempt: 'not_started', + }, + }); - const launched = expect( - launchFromRepo(instance, REF_A, { deadlineAt: 130_000 }) - ).resolves.toEqual({ started: true, startSource: 'image' }); - await vi.waitFor(() => expect(container.startCalls).toHaveLength(1)); - await vi.advanceTimersByTimeAsync(65_000); - container.mainProcess = 'supervisor'; - await vi.advanceTimersByTimeAsync(1_000); - await launched; - } - ); + await expect(launchFromSnapshot(instance, REF_A)).resolves.toEqual({ + started: true, + startSource: 'repository', + }); + expect(container.startCalls[0]).toMatchObject({ containerSnapshot: { id: 'snap-env' } }); + expect(readRecord().startSource).toBe('repository'); + }); - it('does not count a snapshot failure when its deadline expires before native issuance', async () => { - vi.useFakeTimers(); - vi.setSystemTime(10_000); - const key = await indexKeyFor(); - const store = fakeKv({ [key]: { snapshotId: 'snap-good' } }); - const { instance, container, readRecord, setPutGate, releaseHeldPut } = setup({ - env: { REPO_SNAPSHOTS: store.kv }, + it('resumes an interrupted image launch from the image even when a snapshot is offered', async () => { + const { instance, container, readRecord } = setup({ + record: { + ...idleRecord, + state: 'launching', + allocationRef: REF_A, + instance: 'standard-4', + startSource: 'image', + wrapperAttempt: 'not_started', + }, }); - let held = false; - setPutGate(value => { - if ((value as StoredRecord).state !== 'launching') return 'pass'; - held = true; - return 'hold'; + + await expect(launchFromSnapshot(instance, REF_A)).resolves.toEqual({ + started: true, + startSource: 'image', }); + expect(container.startCalls[0]).toMatchObject({ image: IMAGE }); + expect(readRecord().startSource).toBe('image'); + }); - const failed = expect(launchFromRepo(instance, REF_A, { deadlineAt: 15_000 })).rejects.toThrow( - 'wrapper exec timed out' - ); - await vi.waitFor(() => expect(held).toBe(true)); - await vi.advanceTimersByTimeAsync(6_000); - releaseHeldPut(); - await failed; - expect(container.startCalls).toEqual([]); - expect(readRecord().repositoryStartFailures).toBeUndefined(); + it('ignores a session snapshot left in a pre-change record and never takes one on stop', async () => { + const { instance, container, readRecord } = setup({ + record: { + ...idleRecord, + state: 'running', + allocationRef: REF_A, + lastSnapshot: { id: 'legacy-snap', sourceAllocation: 'old' }, + }, + running: true, + }); + + await expect(instance.stop(REF_A)).resolves.toBe('terminal'); + expect(container.snapshotCalls).toBe(0); + expect(readRecord().lastSnapshot).toEqual({ id: 'legacy-snap', sourceAllocation: 'old' }); }); +}); - it('retains the stored snapshot and starts from the image when asked to skip it', async () => { - const key = await indexKeyFor(); - const store = fakeKv({ [key]: { snapshotId: 'snap-bad' } }); - const { instance, container } = setup({ env: { REPO_SNAPSHOTS: store.kv } }); +const RUNTIME_COMMAND = { + argv: ['/bin/sh', '-c', 'exec /bin/sh /usr/local/bin/kilocode-control-plane-supervisor.sh'], +}; - const result = await launchFromRepo(instance, REF_A, { skipRepository: true }); +describe('SandboxContainers runtime release start', () => { + it('runs the installer as PID 1 and confirms the supervisor after its handoff', async () => { + vi.useFakeTimers(); + vi.setSystemTime(10_000); + const { instance, container, readRecord } = setup(); - expect(result).toEqual({ started: true, startSource: 'image' }); - expect(JSON.parse(store.data.get(key) ?? 'null')).toEqual({ snapshotId: 'snap-bad' }); - expect(store.calls.filter(call => call.op === 'delete')).toEqual([]); - expect(container.startCalls[0]).toMatchObject({ image: IMAGE }); + await expect( + instance.launchWrapper({ + allocationRef: REF_A, + env: {}, + instance: 'standard-4', + command: RUNTIME_COMMAND, + }) + ).resolves.toEqual({ started: true, startSource: 'image' }); + + const options = container.startCalls[0]; + expect(options?.entrypoint?.slice(0, 2)).toEqual(['/bin/sh', '-c']); + expect(options?.entrypoint).toEqual(RUNTIME_COMMAND.argv); + + expect(options?.env).toMatchObject(NATIVE_GATE_ENV); + expect(readRecord().state).toBe('running'); }); - it('starts from the image without writing to KV when asked to skip a snapshot', async () => { - const key = await indexKeyFor(); - const store = fakeKv({ [key]: { snapshotId: 'snap-bad' } }); - store.failing.put = true; - const { instance, container } = setup({ env: { REPO_SNAPSHOTS: store.kv } }); + it('installs the selected runtime on a snapshot start too', async () => { + const { instance, container } = setup(); - await expect(launchFromRepo(instance, REF_A, { skipRepository: true })).resolves.toEqual({ + await expect( + launchFromSnapshot(instance, REF_A, { command: RUNTIME_COMMAND }) + ).resolves.toEqual({ started: true, - startSource: 'image', + startSource: 'repository', }); - expect(container.startCalls[0]).toEqual(nativeStartOptions('standard-4')); - expect(JSON.parse(store.data.get(key) ?? 'null')).toEqual({ snapshotId: 'snap-bad' }); - expect(store.calls.filter(call => call.op === 'delete')).toEqual([]); - expect(store.calls.filter(call => call.op === 'put')).toEqual([]); + expect(container.startCalls[0]).toMatchObject({ containerSnapshot: { id: 'snap-env' } }); + expect(container.startCalls[0]?.entrypoint).toEqual(RUNTIME_COMMAND.argv); }); - it('retains a failed entry across starts until a successful capture replaces it', async () => { - const { diagnostics } = captureLogs(); - const key = await indexKeyFor(); - const entry = { snapshotId: 'snap-bad' }; - const store = fakeKv({ [key]: entry }); - const { instance, container } = setup({ - env: { REPO_SNAPSHOTS: store.kv }, - record: { ...idleRecord, repositoryStartFailures: { snapshotId: 'snap-bad', attempts: 10 } }, - }); - - await expect(launchFromRepo(instance, REF_A)).resolves.toMatchObject({ startSource: 'image' }); - expect(JSON.parse(store.data.get(key) ?? 'null')).toEqual(entry); - expect(store.calls.filter(call => call.op === 'put' || call.op === 'delete')).toEqual([]); - expect(diagnostics('snapshot_lookup')[0].fields).toMatchObject({ - source: 'image', - reason: 'retries_exhausted', - attempts: 10, - }); + it('reports a failed snapshot start when the startup command exits', async () => { + const { instance, container } = setup(); + container.runtimeOutcome = { kind: 'exit', code: 72 }; - await expect(instance.captureRepository(REF_A, REPO_KEY)).resolves.toBe(true); - expect(JSON.parse(store.data.get(key) ?? 'null')).toMatchObject({ snapshotId: 'snap-1' }); - await expect(launchFromRepo(instance, REF_B)).resolves.toMatchObject({ + await expect( + launchFromSnapshot(instance, REF_A, { command: RUNTIME_COMMAND }) + ).resolves.toEqual({ + started: false, startSource: 'repository', + snapshotFailed: true, }); - expect(container.startCalls[1]).toMatchObject({ containerSnapshot: { id: 'snap-1' } }); }); - it('uses the recorded source when it resumes a launch that never started the wrapper', async () => { - const key = await indexKeyFor(); - const store = fakeKv({ [key]: { snapshotId: 'snap-repo' } }); - const { instance, container, readRecord } = setup({ - env: { REPO_SNAPSHOTS: store.kv }, - record: { - ...idleRecord, - state: 'launching', + it('throws when the installer exits with a code it does not own', async () => { + const { instance, container } = setup(); + container.runtimeOutcome = { kind: 'exit', code: 1 }; + + await expect( + instance.launchWrapper({ allocationRef: REF_A, + env: {}, instance: 'standard-4', - startSource: 'repository', - wrapperAttempt: 'not_started', - }, + command: RUNTIME_COMMAND, + }) + ).rejects.toThrow('Container stopped before the main process was confirmed'); + }); +}); + +const BUILDER_RUNNER = { url: 'https://worker.example/b', credential: 'builder-credential' }; + +function launchBuilder(instance: SandboxContainers, ref: string, extra = {}) { + return instance.launchBuilder({ + command: { argv: [RUNTIME_BUN_PATH, ENVIRONMENT_BUILDER_PATH] }, + allocationRef: ref, + runner: BUILDER_RUNNER, + instance: 'standard-4', + deadlineAt: Date.now() + 600_000, + ...extra, + }); +} + +describe('SandboxContainers environment builder', () => { + it('starts the builder bundle as PID 1 from the image with internet egress', async () => { + const { instance, container, readRecord } = setup(); + const outbound = attachOutbound(instance); + + await expect(launchBuilder(instance, REF_A)).resolves.toEqual({ + started: true, + startSource: 'image', }); - container.execHandler = cmd => makeExecProcess({ exitCode: cmd[0] === 'pgrep' ? 1 : 0 }); + expect(container.startCalls[0]).toEqual({ + image: IMAGE, + instance: 'standard-4', + enableInternet: true, + entrypoint: [RUNTIME_BUN_PATH, ENVIRONMENT_BUILDER_PATH], + // A snapshot keeps the start env, so the runner never travels in it. + env: { ...NATIVE_GATE_ENV }, + }); + const delivery = container.execCalls.at(-1); + expect(delivery?.cmd.slice(0, 2)).toEqual(['/bin/sh', '-c']); + expect(delivery?.cmd[2]).toContain('umask 077'); + expect(delivery?.cmd[2]).toContain('/run/kilocode-environment-build/runner.json'); + expect(delivery?.cmd[2]).not.toContain(BUILDER_RUNNER.credential); + expect(delivery?.options?.env).toEqual({ + KILO_BUILDER_RUNNER: JSON.stringify(BUILDER_RUNNER), + }); + expect(outbound).not.toHaveBeenCalled(); + expect(readRecord()).toMatchObject({ + state: 'running', + allocationRef: REF_A, + instance: 'standard-4', + }); + const execs = container.execCalls.length; + await expect(launchBuilder(instance, REF_A)).resolves.toEqual({ + started: false, + startSource: 'image', + }); + expect(container.startCalls).toHaveLength(1); + // The delivery script checks whether the runner was already consumed. + expect(container.execCalls).toHaveLength(execs + 1); + }); - const result = await launchFromRepo(instance, REF_A); + it('fails the launch when the runner cannot be delivered', async () => { + const { instance, container } = setup(); + container.execHandler = cmd => makeExecProcess({ exitCode: cmd[0] === '/bin/sh' ? 1 : 0 }); - expect(result).toEqual({ started: true, startSource: 'repository' }); - expect(container.startCalls[0]).toMatchObject({ containerSnapshot: { id: 'snap-repo' } }); - expect(readRecord().startSource).toBe('repository'); + await expect(launchBuilder(instance, REF_A)).rejects.toThrow('Builder runner delivery failed'); + container.execHandler = () => makeExecProcess({ exitCode: 0 }); + await expect(launchBuilder(instance, REF_A)).resolves.toMatchObject({ started: false }); + expect(container.startCalls).toHaveLength(1); + expect(container.execCalls.at(-1)?.cmd[2]).toContain('runner.json.consumed'); }); - it('falls back to the image when the snapshot of an interrupted launch has expired', async () => { - const store = fakeKv(); + it('runs the builder through the runtime installer when a release is selected', async () => { + const { instance, container } = setup(); + + await expect( + launchBuilder(instance, REF_A, { + command: { + argv: ['/bin/sh', '-c', `exec ${RUNTIME_BUN_PATH} ${ENVIRONMENT_BUILDER_PATH}`], + }, + }) + ).resolves.toMatchObject({ + started: true, + }); + const script = container.startCalls[0]?.entrypoint?.[2] ?? ''; + expect(script).toContain('kilocode-environment-builder.js'); + expect(script).toContain(`exec ${RUNTIME_BUN_PATH} ${ENVIRONMENT_BUILDER_PATH}`); + }); + + it('destroys a previous builder allocation before starting the next', async () => { + const { instance, container, readRecord } = setup({ + record: { ...idleRecord, state: 'running', allocationRef: REF_A, instance: 'standard-4' }, + running: true, + }); + + await expect(launchBuilder(instance, REF_B)).resolves.toMatchObject({ started: true }); + expect(container.destroyCalls).toBe(1); + expect(readRecord()).toMatchObject({ state: 'running', allocationRef: REF_B }); + }); + + it('confirms a builder whose start was in flight when the DO restarted', async () => { const { instance, container, readRecord } = setup({ - env: { REPO_SNAPSHOTS: store.kv }, record: { ...idleRecord, state: 'launching', allocationRef: REF_A, instance: 'standard-4', - startSource: 'repository', - wrapperAttempt: 'not_started', + wrapperAttempt: 'exec_pending', }, + running: true, }); - container.execHandler = cmd => makeExecProcess({ exitCode: cmd[0] === 'pgrep' ? 1 : 0 }); + container.mainProcess = 'environment-builder'; - await expect(launchFromRepo(instance, REF_A)).resolves.toEqual({ + await expect(launchBuilder(instance, REF_A)).resolves.toEqual({ started: true, startSource: 'image', }); - expect(container.startCalls[0]).toMatchObject({ image: IMAGE }); - expect(readRecord().startSource).toBe('image'); + expect(container.startCalls).toEqual([]); + expect(readRecord().state).toBe('running'); }); - it('ignores a session snapshot left in a pre-change record and never takes one on stop', async () => { - const { instance, container, readRecord } = setup({ + it('fails closed when an in-flight builder start shows another main process', async () => { + const { instance, container } = setup({ record: { ...idleRecord, - lastSnapshot: { id: 'snap-session', sourceAllocation: REF_A }, - } as StoredRecord, + state: 'launching', + allocationRef: REF_A, + instance: 'standard-4', + wrapperAttempt: 'exec_pending', + }, + running: true, }); + container.mainProcess = 'supervisor'; - await launch(instance, REF_B); - expect(container.startCalls[0]).toMatchObject({ image: IMAGE }); - - await expect(instance.stop(REF_B)).resolves.toBe('terminal'); - expect(container.snapshotCalls).toBe(0); - expect(readRecord().lastSnapshot).toEqual({ id: 'snap-session', sourceAllocation: REF_A }); + await expect(launchBuilder(instance, REF_A)).rejects.toThrow( + 'Main process identity is ambiguous' + ); }); }); -describe('SandboxContainers repository capture', () => { - async function runningWithIndex() { - const store = fakeKv(); - const harness = setup({ env: { REPO_SNAPSHOTS: store.kv } }); - await launchFromRepo(harness.instance, REF_A); - return { ...harness, store }; - } +function runningForCapture() { + return setup({ + record: { ...idleRecord, state: 'running', allocationRef: REF_A }, + running: true, + }); +} + +describe('SandboxContainers capture by id', () => { + it('pins capture compatibility to the running allocation image across a rollout', async () => { + const { instance, container } = setup(); + await launch(instance, REF_A); + container.images.app = 'registry.example/kilo/app:next'; + expect(await instance.captureSnapshot(REF_A, 'image-pin')).toMatchObject({ + status: 'stored', + baseImage: IMAGE, + }); + }); - it('publishes the snapshot with its capture time and size under the repo key and image with a 10 day ttl', async () => { - const { instance, container, store } = await runningWithIndex(); + it('stores the snapshot under its capture id before it replies', async () => { + const { instance, container, storage } = runningForCapture(); container.snapshotBehavior = { kind: 'resolve', id: 'snap-captured' }; - const before = Date.now(); - await expect(instance.captureRepository(REF_A, REPO_KEY, 'deadbeef')).resolves.toBe(true); + const result = await instance.captureSnapshot(REF_A, 'capture-1'); - const key = await indexKeyFor(); - const entry = JSON.parse(store.data.get(key) as string); - expect(entry).toEqual({ + expect(result).toMatchObject({ + status: 'stored', snapshotId: 'snap-captured', - capturedAt: expect.any(Number), size: SNAPSHOT_SIZE, - commit: 'deadbeef', + baseImage: IMAGE, }); - expect(entry.capturedAt).toBeGreaterThanOrEqual(before); - expect(entry.capturedAt).toBeLessThanOrEqual(Date.now()); - expect(store.calls.find(call => call.op === 'put')).toEqual({ - op: 'put', - key, - options: { expirationTtl: 864_000 }, + expect(storage.map.get('containers:capture:v1:capture-1')).toMatchObject({ + state: 'stored', + snapshotId: 'snap-captured', }); + await expect(instance.readCapture('capture-1')).resolves.toEqual(result); + await expect(instance.captureSnapshot(REF_A, 'capture-1')).resolves.toEqual(result); expect(container.snapshotCalls).toBe(1); }); - it('logs one line per capture with its outcome, duration and size, and no key or id', async () => { - const { lines } = captureLogs(); + it('joins a capture that is still running in this isolate', async () => { + const { instance, container } = runningForCapture(); + container.snapshotBehavior = { kind: 'deferred' }; - const stored = await runningWithIndex(); - await stored.instance.captureRepository(REF_A, REPO_KEY, 'abc123'); - const rejected = await runningWithIndex(); - rejected.container.snapshotBehavior = { kind: 'reject' }; - await rejected.instance.captureRepository(REF_A, REPO_KEY); - const unavailable = await runningWithIndex(); - unavailable.store.failing.put = true; - await unavailable.instance.captureRepository(REF_A, REPO_KEY); + const first = instance.captureSnapshot(REF_A, 'capture-1'); + await vi.waitFor(() => expect(container.deferredSnapshots).toHaveLength(1)); + await expect(instance.readCapture('capture-1')).resolves.toEqual({ status: 'in_progress' }); + const second = instance.captureSnapshot(REF_A, 'capture-1'); + container.deferredSnapshots[0]?.resolve('snap-late'); - const captures = lines.filter(line => line.fields.outcome !== undefined); - expect(captures.map(line => [line.level, line.fields.outcome, line.message])).toEqual([ - ['info', 'stored', 'Repository snapshot captured'], - ['warn', 'failed', 'Repository snapshot not saved'], - ['warn', 'index_unavailable', 'Repository snapshot not saved'], - ]); - for (const line of captures) expect(line.fields.durationMs).toEqual(expect.any(Number)); - expect(captures.map(line => line.fields.size)).toEqual([ - SNAPSHOT_SIZE, - undefined, - SNAPSHOT_SIZE, - ]); - expect(JSON.stringify(lines)).not.toContain(REPO_KEY); - expect(JSON.stringify(lines)).not.toContain('abc123'); - expect(JSON.stringify(lines)).not.toContain('snap-1'); + await expect(first).resolves.toMatchObject({ status: 'stored', snapshotId: 'snap-late' }); + await expect(second).resolves.toMatchObject({ status: 'stored', snapshotId: 'snap-late' }); + expect(container.snapshotCalls).toBe(1); }); - it('does nothing without a KV binding, a running container or the running allocation', async () => { - const noIndex = setup(); - await launchFromRepo(noIndex.instance, REF_A); - await expect(noIndex.instance.captureRepository(REF_A, REPO_KEY)).resolves.toBe(false); - expect(noIndex.container.snapshotCalls).toBe(0); + it('reports an issued capture whose call was lost as uncertain and never snapshots again', async () => { + const { instance, container, storage } = runningForCapture(); + storage.map.set('containers:capture:v1:capture-1', { + state: 'issued', + allocationRef: REF_A, + issuedAt: 1, + }); - const { instance, container } = await runningWithIndex(); - await expect(instance.captureRepository(REF_B, REPO_KEY)).resolves.toBe(false); - container.running = false; - await expect(instance.captureRepository(REF_A, REPO_KEY)).resolves.toBe(false); + await expect(instance.readCapture('capture-1')).resolves.toEqual({ + status: 'failed', + reason: 'uncertain', + }); + await expect(instance.captureSnapshot(REF_A, 'capture-1')).resolves.toEqual({ + status: 'failed', + reason: 'uncertain', + }); expect(container.snapshotCalls).toBe(0); }); - it('skips capture for a legacy running allocation whose start image is unknown', async () => { - const store = fakeKv(); - const { instance, container } = setup({ - running: true, - env: { REPO_SNAPSHOTS: store.kv }, - record: { ...idleRecord, state: 'running', allocationRef: REF_A }, + it('refuses a capture for an allocation that is not the running one', async () => { + const { instance, container } = runningForCapture(); + + await expect(instance.captureSnapshot(REF_B, 'capture-1')).resolves.toEqual({ + status: 'failed', + reason: 'not_running', }); - await expect(instance.captureRepository(REF_A, REPO_KEY)).resolves.toBe(false); expect(container.snapshotCalls).toBe(0); - expect(store.data.size).toBe(0); + await expect(instance.readCapture('missing')).resolves.toBeNull(); }); - it('reports a failed snapshot or index write as false and publishes nothing', async () => { - const failedSnapshot = await runningWithIndex(); - failedSnapshot.container.snapshotBehavior = { kind: 'reject' }; - await expect(failedSnapshot.instance.captureRepository(REF_A, REPO_KEY)).resolves.toBe(false); - expect(failedSnapshot.store.data.size).toBe(0); + it('stores a failed snapshot call and a capture abandoned by a stop', async () => { + const failed = runningForCapture(); + failed.container.snapshotBehavior = { kind: 'reject' }; + await expect(failed.instance.captureSnapshot(REF_A, 'capture-1')).resolves.toEqual({ + status: 'failed', + reason: 'snapshot_failed', + }); - const failedWrite = await runningWithIndex(); - failedWrite.store.failing.put = true; - await expect(failedWrite.instance.captureRepository(REF_A, REPO_KEY)).resolves.toBe(false); - expect(failedWrite.store.data.size).toBe(0); + const abandoned = runningForCapture(); + abandoned.container.snapshotBehavior = { kind: 'deferred' }; + const capture = abandoned.instance.captureSnapshot(REF_A, 'capture-2'); + await vi.waitFor(() => expect(abandoned.container.deferredSnapshots).toHaveLength(1)); + await expect(abandoned.instance.stop(REF_A)).resolves.toBe('terminal'); + abandoned.container.deferredSnapshots[0]?.resolve('snap-after-stop'); + await expect(capture).resolves.toEqual({ status: 'failed', reason: 'abandoned' }); }); it('gives up on a capture that never finishes and does not block a stop', async () => { vi.useFakeTimers(); - const { instance, container, store, readRecord } = await runningWithIndex(); + const { instance, container } = runningForCapture(); container.snapshotBehavior = { kind: 'deferred' }; - const capture = instance.captureRepository(REF_A, REPO_KEY); - await vi.advanceTimersByTimeAsync(0); - expect(container.snapshotCalls).toBe(1); - + const capture = instance.captureSnapshot(REF_A, 'capture-1'); + await vi.waitFor(() => expect(container.deferredSnapshots).toHaveLength(1)); await expect(instance.stop(REF_A)).resolves.toBe('terminal'); - expect(readRecord()).toMatchObject({ state: 'idle', allocationRef: null }); - - await vi.advanceTimersByTimeAsync(5 * 60_000 + 1_000); - await expect(capture).resolves.toBe(false); - container.deferredSnapshots[0]?.resolve('late'); - await vi.advanceTimersByTimeAsync(0); - expect(store.data.size).toBe(0); + await vi.advanceTimersByTimeAsync(5 * 60_000); + await expect(capture).resolves.toEqual({ status: 'failed', reason: 'snapshot_failed' }); }); - it('does not publish a snapshot that finishes after the allocation stopped', async () => { - const { instance, container, store } = await runningWithIndex(); - container.snapshotBehavior = { kind: 'deferred' }; - - const capture = instance.captureRepository(REF_A, REPO_KEY); - await vi.waitFor(() => expect(container.snapshotCalls).toBe(1)); - await instance.stop(REF_A); - container.deferredSnapshots[0]?.resolve('late'); - - await expect(capture).resolves.toBe(false); - expect(store.data.size).toBe(0); + it('retains a bounded number of capture records', async () => { + const { instance, storage } = runningForCapture(); + for (let index = 0; index < 10; index += 1) { + await instance.captureSnapshot(REF_A, `capture-${index}`); + } + const kept = [...storage.map.keys()].filter(key => key.startsWith('containers:capture:v1:')); + expect(kept).toHaveLength(8); + expect(kept).not.toContain('containers:capture:v1:capture-0'); + expect(storage.map.get('containers:captures:v1')).toEqual( + Array.from({ length: 8 }, (_, index) => `capture-${9 - index}`) + ); }); }); diff --git a/services/cloud-agent-next/src/sandbox-containers/snapshot-start-failures.ts b/services/cloud-agent-next/src/sandbox-containers/snapshot-start-failures.ts new file mode 100644 index 00000000000..ff27ffa580e --- /dev/null +++ b/services/cloud-agent-next/src/sandbox-containers/snapshot-start-failures.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +export const SNAPSHOT_START_FAILURES_KEY = 'containers:snapshot-start-failures:v1'; +export const MAX_SNAPSHOT_START_FAILURES = 3; +const MAX_FAILED_SNAPSHOTS = 16; +const schema = z + .array(z.object({ snapshotId: z.string(), failures: z.number().int().nonnegative() })) + .max(MAX_FAILED_SNAPSHOTS); +export type SnapshotStartFailures = z.infer; + +export function parseSnapshotStartFailures(value: unknown): SnapshotStartFailures { + const result = schema.safeParse(value); + return result.success ? result.data : []; +} + +export function recordSnapshotStartFailure( + records: SnapshotStartFailures, + snapshotId: string +): SnapshotStartFailures { + const failures = records.find(record => record.snapshotId === snapshotId)?.failures ?? 0; + return [ + { snapshotId, failures: Math.min(MAX_SNAPSHOT_START_FAILURES, failures + 1) }, + ...records.filter(record => record.snapshotId !== snapshotId), + ].slice(0, MAX_FAILED_SNAPSHOTS); +} diff --git a/services/cloud-agent-next/src/sandbox-control/cloudflare-containers-provider.ts b/services/cloud-agent-next/src/sandbox-control/cloudflare-containers-provider.ts index 6afbe0b7822..ad39d6ff120 100644 --- a/services/cloud-agent-next/src/sandbox-control/cloudflare-containers-provider.ts +++ b/services/cloud-agent-next/src/sandbox-control/cloudflare-containers-provider.ts @@ -52,6 +52,10 @@ export function createCloudflareContainersProviderAdapter(deps: { instance?: ContainerInstanceSize; egressAllowlist?: string[]; getContainer: (logicalSandboxId: string) => DurableObjectStub; + /** + * Trusted cold-launch inputs. Absent, and a descriptor absent within it, keep + * the baked supervisor selection. + */ }): ProviderAdapter { const instance = deps.instance ?? CLOUDFLARE_CONTAINERS_DEFAULT_INSTANCE; const encodeIntentProviderRef = (intent: ProviderAllocationIntent): string => @@ -116,6 +120,7 @@ export function createCloudflareContainersProviderAdapter(deps: { }; return { + environmentSnapshots: true, ensureBillingAdmission, async isBillingBlocked(ref) { if (ref === null || decodeOwnedProviderRef(ref) === null) return false; @@ -145,14 +150,16 @@ export function createCloudflareContainersProviderAdapter(deps: { const workloadLimitMb = env['CONTROL_WORKLOAD_LIMIT_MB'] ?? String(containersBillingIdentity(instance).capacity.memoryMiB); + // One launch selection (amendment "cold session without a snapshot"): a + // trusted descriptor installs the verified runtime before the supervisor, + // also on a snapshot start; no descriptor runs the baked supervisor. const { startSource, snapshotFailed } = await container.launchWrapper({ allocationRef: ref, command, instance, containment: owned.containment, ...(deps.egressAllowlist === undefined ? {} : { egressAllowlist: deps.egressAllowlist }), - ...(options?.repoKey === undefined ? {} : { repoKey: options.repoKey }), - ...(options?.skipRepository === true ? { skipRepository: true as const } : {}), + ...(options?.snapshot === undefined ? {} : { snapshot: options.snapshot }), ...(options?.deadlineAt === undefined ? {} : { deadlineAt: options.deadlineAt }), env: { ...env, @@ -178,16 +185,6 @@ export function createCloudflareContainersProviderAdapter(deps: { }); return { startSource }; }, - async captureRepository(ref, repoKey, commit) { - if (decodeOwnedProviderRef(ref) === null) return false; - try { - return await deps - .getContainer(deps.logicalSandboxId) - .captureRepository(ref, repoKey, commit); - } catch { - return false; - } - }, async observe(ref, intent) { const providerRef = resolveProviderRef(ref, intent); if (providerRef === null) return { status: 'unknown' }; diff --git a/services/cloud-agent-next/src/sandbox-control/container-paths.ts b/services/cloud-agent-next/src/sandbox-control/container-paths.ts index 0543c911089..d6436fe30b9 100644 --- a/services/cloud-agent-next/src/sandbox-control/container-paths.ts +++ b/services/cloud-agent-next/src/sandbox-control/container-paths.ts @@ -19,5 +19,13 @@ export const CONTROL_WRAPPER_PATH = runtimeInstallPath( ); export const CONTROL_WRAPPER_LOG_PATH = '/tmp/kilocode-control-wrapper.log'; +/** + * The environment builder bundle. A builder container runs it as its main + * process (`RUNTIME_BUN_PATH` + this path), never through the session supervisor. + */ +export const ENVIRONMENT_BUILDER_PATH = runtimeInstallPath( + wrapperBundle('src/environment-builder/main.ts').installName +); + /** pgrep pattern matching the supervisor or its wrapper child. */ export const CONTROL_PROCESS_MATCH = `${CONTROL_SUPERVISOR_PATH}|${CONTROL_WRAPPER_PATH}`; 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..050937bf961 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 @@ -118,30 +118,43 @@ describe('startup selection and preflight', () => { }) ).rejects.toMatchObject({ permanentReason: 'invalid_configuration' }); }); - it('renders bounded commands using the Worker host and the latest first-connect deadline', async () => { - const deadlineAt = Date.now() + 420123; - const command = await downloadedStartup({ - env: { - WORKER_URL: 'https://worker.test', - NEXTAUTH_SECRET: secret, - RUNTIME_RELEASE_BUCKET: { - async head() { - return { size: 1 }; - }, - } as unknown as R2Bucket, - }, - descriptor: text, - sandboxId: 'sbx_test', - allocationId: crypto.randomUUID(), - deadlineAt, - }); - expect(command.argv[0]).toBe('/bin/sh'); - expect(command.argv.every(arg => new TextEncoder().encode(arg).byteLength < 128 * 1024)).toBe( - true - ); - // Do not include command or token values in assertion output. - expect(command.argv.some(arg => arg.includes('https://worker.test'))).toBe(true); - }); + it.each([undefined, 'environment-builder'] as const)( + 'renders bounded %s commands using the Worker host', + async mainProcess => { + const deadlineAt = Date.now() + 420123; + const command = await downloadedStartup({ + env: { + WORKER_URL: 'https://worker.test', + NEXTAUTH_SECRET: secret, + RUNTIME_RELEASE_BUCKET: { + async head() { + return { size: 1 }; + }, + } as unknown as R2Bucket, + }, + descriptor: text, + sandboxId: 'sbx_test', + allocationId: crypto.randomUUID(), + deadlineAt, + mainProcess, + }); + expect( + command.argv.some(arg => + arg.includes( + mainProcess === 'environment-builder' + ? 'kilocode-environment-builder.js' + : 'kilocode-control-plane-supervisor.sh' + ) + ) + ).toBe(true); + expect(command.argv[0]).toBe('/bin/sh'); + expect(command.argv.every(arg => new TextEncoder().encode(arg).byteLength < 128 * 1024)).toBe( + true + ); + // Do not include command or token values in assertion output. + expect(command.argv.some(arg => arg.includes('https://worker.test'))).toBe(true); + } + ); it('rejects an escaped bootstrap argument above the Linux limit even when the descriptor fits', async () => { const large = structuredClone(descriptor); const control = large.artifacts.find(artifact => artifact.role === 'control')!; 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..e704fd1b862 100644 --- a/services/cloud-agent-next/src/sandbox-control/downloaded-startup.ts +++ b/services/cloud-agent-next/src/sandbox-control/downloaded-startup.ts @@ -16,6 +16,7 @@ export async function downloadedStartup(input: { sandboxId: string; allocationId: string; deadlineAt: number; + mainProcess?: 'environment-builder'; }): Promise { const release = await loadRuntimeRelease(input.descriptor, input.env.RUNTIME_RELEASE_BUCKET); const secret = await resolveSecret(input.env.NEXTAUTH_SECRET); @@ -40,6 +41,7 @@ export async function downloadedStartup(input: { allocationId: input.allocationId, grant, deadlineAt: input.deadlineAt, + mainProcess: input.mainProcess, }); } catch { throw new ProviderCreationError('invalid_configuration'); 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..3cde5626823 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 @@ -110,10 +110,8 @@ type ContractHarness = { leaseCalls(): number; /** Lease durations the backend received where it has one (Cloudflare has none). */ leaseMs(): number[]; - /** Containers-only repository capture; absent on the other two adapters. */ - captureRepository?(ref: string, repoKey: string, commit?: string): Promise; - /** Containers-only observable capture call. */ - capturedRepository?(): { ref: string; repoKey: string; commit?: string } | undefined; + /** Containers-only: the launch inputs the container DO received. */ + launchInputs?(): ContainersLaunchInput[]; /** Containers-only: set the outcome the backend reports from `stop`. */ setStopOutcome?(outcome: 'terminal' | 'retryable'): void; /** Vercel-only: authoritative contained-grant reads observed by the adapter. */ @@ -220,7 +218,6 @@ function containersHarness( admission: async (): Promise => ({ success: true }), launchInputs: [] as ContainersLaunchInput[], leaseMs: [] as number[], - captures: [] as { ref: string; repoKey: string; commit?: string }[], billingBlocked: false, billingReads: 0, }; @@ -240,10 +237,6 @@ function containersHarness( state.leaseMs.push(ms); if (options.failLaunchLease) throw new Error('containers lease failed'); }, - captureRepository: async (ref: string, repoKey: string, commit?: string) => { - state.captures.push({ ref, repoKey, ...(commit === undefined ? {} : { commit }) }); - return true; - }, observe: async () => { if (state.observeThrows) throw new Error('containers observe failed'); return state.observation; @@ -295,8 +288,7 @@ function containersHarness( launchInvocation: () => state.launchInputs.at(-1)?.command.argv, leaseCalls: () => state.leaseMs.length, leaseMs: () => [...state.leaseMs], - captureRepository: (ref, repoKey, commit) => adapter.captureRepository!(ref, repoKey, commit), - capturedRepository: () => state.captures.at(-1), + launchInputs: () => [...state.launchInputs], setStopOutcome: outcome => { state.stopResult = outcome; }, @@ -580,24 +572,24 @@ describe('provider-specific contract differences', () => { const owned = harness.leaseCalls(); await harness.adapter.ensureLeaseAtLeast(harness.foreignRef(), OWNED_LEASE_MS); expect(harness.leaseCalls()).toBe(owned); - expect('captureRepository' in harness.adapter).toBe(false); + expect('environmentSnapshots' in harness.adapter).toBe(false); expect('applyContainedCredentials' in harness.adapter).toBe(false); }); - it('maps Containers repository capture and reports a backend stop outcome verbatim', async () => { + it('forwards an environment snapshot start and reports a backend stop outcome verbatim', async () => { const harness = containersHarness(); const ref = await harness.create(); - expect(await harness.captureRepository!(ref, 'repo-key', 'commit-1')).toBe(true); - expect(harness.capturedRepository!()).toEqual({ - ref, - repoKey: 'repo-key', - commit: 'commit-1', - }); - expect(await harness.captureRepository!(harness.foreignRef(), 'repo-key')).toBe(false); + expect(harness.adapter.environmentSnapshots).toBe(true); + const snapshot = { snapshotId: 'snap-env', baseImage: 'registry.example/app:1' }; + await harness.adapter.launch(ref, { command: preinstalledStartup(), env: {} }, { snapshot }); + expect(harness.launchInputs!().at(-1)?.snapshot).toEqual(snapshot); + await harness.adapter.launch(ref, { command: preinstalledStartup(), env: {} }); + expect(harness.launchInputs!().at(-1)).not.toHaveProperty('snapshot'); + const launchLeases = harness.leaseMs().length; await harness.adapter.ensureLeaseAtLeast(ref, OWNED_LEASE_MS); - expect(harness.leaseMs()).toEqual([OWNED_LEASE_MS]); + expect(harness.leaseMs().slice(launchLeases)).toEqual([OWNED_LEASE_MS]); expect(await harness.adapter.logs(ref)).toBe('container-log-body'); // The DO reports an unconfirmed stop outcome, and the adapter returns it verbatim. harness.setStopOutcome!('retryable'); @@ -659,7 +651,7 @@ describe('provider-specific contract differences', () => { const leases = harness.leaseMs().length; await harness.adapter.ensureLeaseAtLeast(harness.foreignRef(), OWNED_LEASE_MS); expect(harness.leaseMs()).toHaveLength(leases); - expect('captureRepository' in harness.adapter).toBe(false); + expect('environmentSnapshots' in harness.adapter).toBe(false); await expect( harness.adapter.applyContainedCredentials!(ref, [] as readonly SessionCredentialGrant[]) 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..cc11ebcdce8 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 @@ -107,11 +107,7 @@ describe('native egress policy forwarding', () => { SANDBOX_CONTROL_URL: 'wss://agent.example.com', }, }); - await provider.launch( - created.providerRef, - { command: preinstalledStartup(), env: {} }, - { skipRepository: true } - ); + await provider.launch(created.providerRef, { command: preinstalledStartup(), env: {} }); for (const [input] of launchWrapper.mock.calls) { if (egressAllowlist === undefined) expect(input).not.toHaveProperty('egressAllowlist'); else expect(input.egressAllowlist).toEqual(egressAllowlist); diff --git a/services/cloud-agent-next/src/sandbox-control/provider.ts b/services/cloud-agent-next/src/sandbox-control/provider.ts index 7340418d87f..bc25e89b8e7 100644 --- a/services/cloud-agent-next/src/sandbox-control/provider.ts +++ b/services/cloud-agent-next/src/sandbox-control/provider.ts @@ -90,14 +90,22 @@ export type ProviderObservation = { providerRef?: string; }; -/** What a physical start used: the image, or a repository snapshot of it. */ +/** What a physical start used: the image, or an environment snapshot of it. */ export type ProviderStartSource = 'image' | 'repository'; +/** + * A published environment snapshot the Sandbox DO selected for this allocation. + * Only the environment coordinator produces one; no public API accepts it. + */ +export type ProviderSnapshotStart = { + snapshotId: string; + /** The image the snapshot was captured from. */ + baseImage: string; +}; + export type ProviderLaunchOptions = { - /** Keyed hash of the launch's scope, repository and env; a provider may start from its snapshot. */ - repoKey?: string; - /** Start from the image and retain the failed snapshot for debugging. */ - skipRepository?: true; + /** Start from this snapshot; a provider without snapshot support ignores it. */ + snapshot?: ProviderSnapshotStart; deadlineAt?: number; }; @@ -131,12 +139,8 @@ export type ProviderAdapter = { stop(ref: string | null, intent?: ProviderAllocationIntent | null): Promise; ensureLeaseAtLeast(ref: string, ms: number): Promise; logs(ref: string): Promise; - /** - * Save the running container as the repository snapshot for `repoKey`. Only a - * provider that can start from one implements it; `false` is a failed capture - * that the caller ignores. - */ - captureRepository?(ref: string, repoKey: string, commit?: string): Promise; + /** Present on a provider that can start a session from an environment snapshot. */ + readonly environmentSnapshots?: true; /** * Replace the live sandbox's contained-credential network policy with one * built from the authoritative grants. Only a provider whose network policy diff --git a/services/cloud-agent-next/src/sandbox-control/runtime-bootstrap.ts b/services/cloud-agent-next/src/sandbox-control/runtime-bootstrap.ts index df041b60b24..faa659c4905 100644 --- a/services/cloud-agent-next/src/sandbox-control/runtime-bootstrap.ts +++ b/services/cloud-agent-next/src/sandbox-control/runtime-bootstrap.ts @@ -332,6 +332,7 @@ export function renderRuntimeBootstrap(input: { allocationId: string; grant: string; deadlineAt: number; + mainProcess?: 'environment-builder'; }): ProviderStartupCommand { const descriptor = parseRuntimeRelease(input.descriptor); serializeRuntimeRelease(descriptor); @@ -351,6 +352,14 @@ export function renderRuntimeBootstrap(input: { ) throw new Error('Invalid runtime startup URLs'); if (!Number.isSafeInteger(input.deadlineAt)) throw new Error('Invalid runtime deadline'); + const mainPath = + input.mainProcess === 'environment-builder' + ? runtimeInstallPath('kilocode-environment-builder.js') + : runtimeInstallPath(CONTROL_PLANE_SUPERVISOR.installName); + const mainArgv = + input.mainProcess === 'environment-builder' + ? `/usr/local/bin/bun ${shellQuote(mainPath)}` + : `/bin/sh ${shellQuote(mainPath)}`; const config = JSON.stringify({ ...input, descriptor, @@ -398,10 +407,10 @@ export function renderRuntimeBootstrap(input: { `/usr/bin/flock -n -E ${STARTUP_FAILURES.install_interrupted.exitCode} 9`, `cli_path=$(${installNode} install)`, 'unset bootstrap_config', - `if [ ! -f ${shellQuote(runtimeInstallPath(CONTROL_PLANE_SUPERVISOR.installName))} ]; then exit ${STARTUP_FAILURES.supervisor_missing.exitCode}; fi`, + `if [ ! -f ${shellQuote(mainPath)} ]; then exit ${STARTUP_FAILURES.supervisor_missing.exitCode}; fi`, `${node} ${shellQuote(PROBE)} "$cli_path" ${shellQuote(descriptor.cliVersion)} ${shellQuote(String(input.deadlineAt))}`, 'trap - 0', - `exec ${environment} KILO_RUNTIME_CLI_PATH="$cli_path" /bin/sh ${shellQuote(runtimeInstallPath(CONTROL_PLANE_SUPERVISOR.installName))}`, + `exec ${environment} KILO_RUNTIME_CLI_PATH="$cli_path" ${mainArgv}`, ].join('\n'); return { argv: ['/bin/sh', '-c', script] }; } diff --git a/services/cloud-agent-next/src/server.ts b/services/cloud-agent-next/src/server.ts index 243fc5c8016..cf3b24c176a 100644 --- a/services/cloud-agent-next/src/server.ts +++ b/services/cloud-agent-next/src/server.ts @@ -47,6 +47,7 @@ import { PtyIdSchema, sessionIdSchema } from './router/schemas.js'; import { registerRuntimeArtifactRoutes } from './sandbox-control/runtime-artifact-routes.js'; import { registerStartupFailureRoute } from './sandbox-control/startup-failure.js'; import { registerControlLogRoutes } from './sandbox-control/log-routes.js'; +import { registerEnvironmentBuildRoutes } from './environment-build/routes.js'; import { runtimeCredentialProxyFacadeBaseUrl, runtimeCredentialProxyUpstream, @@ -282,6 +283,7 @@ function requireInternalApi(c: Context): Response | null { registerControlLogRoutes(app); registerRuntimeArtifactRoutes(app); registerStartupFailureRoute(app); +registerEnvironmentBuildRoutes(app); 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/session-registration-metadata.ts b/services/cloud-agent-next/src/session/session-registration-metadata.ts index a9236b1b860..da3e93e29e2 100644 --- a/services/cloud-agent-next/src/session/session-registration-metadata.ts +++ b/services/cloud-agent-next/src/session/session-registration-metadata.ts @@ -28,6 +28,7 @@ export type GroupedRegisterSessionInput = { }; repository?: SessionRepositoryRequest; profile?: SessionProfileBundle; + environment?: SessionMetadata['environment']; finalization?: SessionFinalization; callback?: SessionMetadata['callback']; workspace?: Pick< @@ -199,6 +200,7 @@ export async function buildSessionMetadataFromRegistration( }, finalization: input.finalization, profile: input.profile, + ...(input.environment ? { environment: input.environment } : {}), callback: input.callback, workspace: { ...input.workspace, diff --git a/services/cloud-agent-next/src/session/session-registration.ts b/services/cloud-agent-next/src/session/session-registration.ts index abac04dfa7e..8037b053074 100644 --- a/services/cloud-agent-next/src/session/session-registration.ts +++ b/services/cloud-agent-next/src/session/session-registration.ts @@ -1150,6 +1150,7 @@ function buildSessionRegistrationCommand( }, repository: input.repository, profile: input.profile?.resolved, + ...(input.environment ? { environment: input.environment } : {}), finalization: input.finalization, callback: input.options?.callbackTarget ? { target: input.options.callbackTarget } : undefined, workspace: { diff --git a/services/cloud-agent-next/src/session/session-requests.ts b/services/cloud-agent-next/src/session/session-requests.ts index 6ddc5d9648e..b8bc66aa515 100644 --- a/services/cloud-agent-next/src/session/session-requests.ts +++ b/services/cloud-agent-next/src/session/session-requests.ts @@ -7,6 +7,7 @@ import type { SessionFinalization, } from '../execution/types.js'; import type { SessionProfileBundle } from '../session-profile.js'; +import type { SessionEnvironmentReference } from '../environment-build/reference.js'; export type ProfileOverrides = { envVars?: Record; @@ -73,9 +74,16 @@ export type SessionCreateRequest = { cloneUpToMessageId?: string; }; /** - * The saved allocation key of the repository's environment, set only by - * preflight and never by a client. It is the first saved candidate; - * eligibility is checked when the saved allocation is resolved. + * The authorized reference of the repository's environment, set only by + * preflight after it resolved the environment from the owner and the + * repository. Persisted on session metadata and carried DO-private to the + * Sandbox DO for snapshot selection. + */ + environment?: SessionEnvironmentReference; + /** + * The environment's saved allocation key, set only by preflight and never by + * a client. It is the first saved candidate; eligibility is checked when the + * saved allocation is resolved. */ environmentSandboxAllocation?: string; profile?: { 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..f7964fe8634 100644 --- a/services/cloud-agent-next/src/shared/control-plane-protocol.ts +++ b/services/cloud-agent-next/src/shared/control-plane-protocol.ts @@ -32,14 +32,16 @@ export const CONTROL_PLANE_GIT_PLATFORMS = ['github', 'gitlab', 'bitbucket'] as export type ControlPlaneGitPlatform = (typeof CONTROL_PLANE_GIT_PLATFORMS)[number]; /** - * Route preparation steps. `sandbox_create`, `sandbox_start` and - * `snapshot_fallback` come from the Sandbox DO's allocation (provider create, - * then waiting for the wrapper's `hello`, and starting from the image when a - * repository snapshot start failed); the rest come from the wrapper's - * `session.progress`. + * Route preparation steps. `sandbox_create`, `environment_snapshot`, + * `sandbox_start` and `snapshot_fallback` come from the Sandbox DO's allocation + * (provider create, the environment snapshot decision, then waiting for the + * wrapper's `hello`, and starting from the image when a snapshot start failed); + * the rest come from the wrapper's `session.progress`. `snapshot` is only sent + * by wrappers that still capture. */ export const CONTROL_PLANE_PREPARATION_STEPS = [ 'sandbox_create', + 'environment_snapshot', 'sandbox_start', 'snapshot_fallback', 'clone', diff --git a/services/cloud-agent-next/src/shared/environment-build-protocol.ts b/services/cloud-agent-next/src/shared/environment-build-protocol.ts new file mode 100644 index 00000000000..552eaa62158 --- /dev/null +++ b/services/cloud-agent-next/src/shared/environment-build-protocol.ts @@ -0,0 +1,130 @@ +import { z } from 'zod'; +import { CONTROL_PLANE_GIT_PLATFORMS } from './control-plane-protocol.js'; + +/** + * Environment builder <-> Worker protocol (plan "Build admission and + * execution"). The builder is a dedicated runner: it has no Session DO, Kilo + * runtime, model token or transcript. It talks to its environment coordinator + * through short authenticated HTTP calls bound to one environment, build and + * allocation, so no request stays open while setup runs. + * + * Shared by the wrapper bundle and the Worker, so it imports nothing Worker-only. + */ + +/** The fixed isolated workspace a builder prepares; the same path every session uses. */ +export const ENVIRONMENT_BUILD_WORKSPACE = '/workspace/app'; + +export const ENVIRONMENT_BUILD_LOG_PATH = '/tmp/kilocode-environment-builder.log'; + +/** + * Where the builder's endpoint and credential arrive. The container DO writes + * the file through an exec after the start, never as start env: a snapshot keeps + * the container's start configuration, so a restored session would inherit it. + * The builder deletes the file as soon as it has read it. + */ +export const ENVIRONMENT_BUILD_RUNNER_DIR = '/run/kilocode-environment-build'; +export const ENVIRONMENT_BUILD_RUNNER_PATH = `${ENVIRONMENT_BUILD_RUNNER_DIR}/runner.json`; + +export const ENVIRONMENT_BUILD_RUNNER_DELIVERY_SCRIPT = [ + 'umask 077', + `mkdir -p ${ENVIRONMENT_BUILD_RUNNER_DIR}`, + `{ [ ! -e ${ENVIRONMENT_BUILD_RUNNER_PATH}.consumed ] || exit 0; }`, + `printf '%s' "$KILO_BUILDER_RUNNER" > ${ENVIRONMENT_BUILD_RUNNER_PATH}.tmp`, + `mv ${ENVIRONMENT_BUILD_RUNNER_PATH}.tmp ${ENVIRONMENT_BUILD_RUNNER_PATH}`, + `if [ -e ${ENVIRONMENT_BUILD_RUNNER_PATH}.consumed ]; then rm -f ${ENVIRONMENT_BUILD_RUNNER_PATH}; fi`, +].join(' && '); + +export const environmentBuildRunnerSchema = z + .object({ url: z.string().url(), credential: z.string().min(1) }) + .strict(); +export type EnvironmentBuildRunner = z.infer; + +/** How often a running builder reports liveness. */ +export const ENVIRONMENT_BUILD_HEARTBEAT_MS = 15_000; +/** Bound on the text of one log batch, after redaction. */ +export const ENVIRONMENT_BUILD_LOG_BATCH_MAX_BYTES = 64 * 1024; +export const ENVIRONMENT_BUILD_LOG_BATCH_MAX_LINES = 512; +/** Bound on one log line the builder forwards. */ +export const ENVIRONMENT_BUILD_LOG_LINE_MAX_CHARS = 8_192; + +export const environmentBuildGitSchema = z + .object({ + url: z.string().min(1).max(2048), + token: z.string().min(1).max(4096).optional(), + platform: z.enum(CONTROL_PLANE_GIT_PLATFORMS).optional(), + }) + .strict(); + +/** What the builder fetches once it starts. Secrets are decrypted only into this reply. */ +export const environmentBuildInputSchema = z + .object({ + environmentId: z.string().uuid(), + buildId: z.string().uuid(), + allocationId: z.string().uuid(), + git: environmentBuildGitSchema, + directory: z.literal(ENVIRONMENT_BUILD_WORKSPACE), + commands: z.array(z.string().min(1).max(16_000)).max(20), + env: z.record(z.string().min(1).max(128), z.string().max(10_000)), + /** Names of the secret entries in `env`, so the builder redacts their values. */ + secretKeys: z.array(z.string().min(1).max(128)).max(50), + deadlineAt: z.number().int().positive(), + }) + .strict(); +export type EnvironmentBuildInput = z.infer; + +/** Steps the builder reports; the coordinator's own states wrap them. */ +export const ENVIRONMENT_BUILD_RUNNER_STEPS = ['clone', 'setup', 'cleanup'] as const; +export type EnvironmentBuildRunnerStep = (typeof ENVIRONMENT_BUILD_RUNNER_STEPS)[number]; + +export const environmentBuildEventsSchema = z + .object({ + /** Monotonic per builder process; a replayed batch is applied once. */ + sequence: z.number().int().nonnegative(), + step: z.enum(ENVIRONMENT_BUILD_RUNNER_STEPS).optional(), + /** Redacted output lines, in order. */ + logs: z + .array(z.string().max(ENVIRONMENT_BUILD_LOG_LINE_MAX_CHARS)) + .max(ENVIRONMENT_BUILD_LOG_BATCH_MAX_LINES) + .optional(), + }) + .strict(); +export type EnvironmentBuildEvents = z.infer; + +/** The coordinator's answer to each events call: keep going, or stop work now. */ +export const environmentBuildEventsReplySchema = z + .object({ action: z.enum(['continue', 'cancel']) }) + .strict(); +export type EnvironmentBuildEventsReply = z.infer; + +/** Closed failure reasons a builder can report; each maps to user-visible text. */ +export const ENVIRONMENT_BUILD_RUNNER_FAILURES = [ + 'input_unavailable', + 'clone_failed', + 'setup_failed', + 'setup_timeout', + 'cleanup_failed', + 'canceled', +] as const; +export type EnvironmentBuildRunnerFailure = (typeof ENVIRONMENT_BUILD_RUNNER_FAILURES)[number]; + +export const environmentBuildResultSchema = z.discriminatedUnion('outcome', [ + z + .object({ + outcome: z.literal('ready'), + /** The exact default-branch commit the workspace was prepared at. */ + commit: z.string().regex(/^[0-9a-f]{40}$/), + branch: z.string().min(1).max(256), + setupDurationMs: z.number().int().nonnegative(), + }) + .strict(), + z + .object({ + outcome: z.literal('failed'), + step: z.enum(ENVIRONMENT_BUILD_RUNNER_STEPS), + reason: z.enum(ENVIRONMENT_BUILD_RUNNER_FAILURES), + /** A command index or a short safe phrase; never output, values or URLs. */ + detail: z.string().max(200).optional(), + }) + .strict(), +]); +export type EnvironmentBuildResult = z.infer; diff --git a/services/cloud-agent-next/src/shared/environment-build-status.ts b/services/cloud-agent-next/src/shared/environment-build-status.ts new file mode 100644 index 00000000000..0ce668b388e --- /dev/null +++ b/services/cloud-agent-next/src/shared/environment-build-status.ts @@ -0,0 +1,132 @@ +import { z } from 'zod'; + +/** + * Public environment build contracts (plan "Build admission and execution"), + * shared by the Worker's tRPC handlers and the web client. They return status + * and metadata only: no snapshot handle, no secret, no command text and no log + * of credentials ever crosses this boundary. + */ + +export const ENVIRONMENT_BUILD_PUBLIC_STATES = [ + 'queued', + 'preparing', + 'capturing', + 'publishing', + 'stopping', + 'succeeded', + 'failed', + 'canceled', + 'obsolete', +] as const; + +export const environmentBuildSummarySchema = z + .object({ + buildId: z.string().uuid(), + revision: z.number().int().positive(), + trigger: z.enum(['manual', 'session']), + state: z.enum(ENVIRONMENT_BUILD_PUBLIC_STATES), + /** The terminal state a stopping build enters once cleanup is confirmed. */ + outcome: z.enum(['succeeded', 'failed', 'canceled', 'obsolete']).nullable(), + step: z.enum(['clone', 'setup', 'cleanup']).nullable(), + failureReason: z.string().nullable(), + failureDetail: z.string().nullable(), + sourceCommit: z.string().nullable(), + sourceBranch: z.string().nullable(), + setupDurationMs: z.number().int().nonnegative().nullable(), + createdAt: z.number().int(), + finishedAt: z.number().int().nullable(), + cleanup: z.enum(['not_started', 'pending', 'confirmed', 'failing']), + logTruncated: z.boolean(), + }) + .strict(); +export type EnvironmentBuildSummary = z.infer; + +export const environmentSnapshotSummarySchema = z + .object({ + buildId: z.string().uuid(), + revision: z.number().int().positive(), + sourceCommit: z.string(), + sourceBranch: z.string(), + capturedAt: z.number().int(), + size: z.number().nonnegative().nullable(), + setupDurationMs: z.number().int().nonnegative(), + /** `revision`: a newer configuration exists; `age`: older than the refresh age. */ + /** `image`: built for another sandbox image or runtime ABI, so sessions start without it. */ + stale: z.enum(['revision', 'age', 'image']).nullable(), + expiresAt: z.number().int(), + }) + .strict(); +export type EnvironmentSnapshotSummary = z.infer; + +export const environmentBuildStatusSchema = z + .object({ + environmentId: z.string().uuid(), + /** False when the environment's provider cannot build snapshots. */ + supported: z.boolean(), + unsupportedReason: z.string().nullable(), + configurationRevision: z.number().int().positive(), + activeBuild: environmentBuildSummarySchema.nullable(), + latestBuild: environmentBuildSummarySchema.nullable(), + published: environmentSnapshotSummarySchema.nullable(), + automaticBackoff: z + .object({ + revision: z.number().int().positive(), + failures: z.number().int().nonnegative(), + nextAttemptAt: z.number().int(), + }) + .strict() + .nullable(), + /** A fresh session start that takes longer than this times out. */ + sessionPreparationBudgetMs: z.number().int().positive(), + }) + .strict(); +export type EnvironmentBuildStatus = z.infer; + +export const environmentBuildLogsSchema = z + .object({ + buildId: z.string().uuid(), + lines: z.array(z.object({ seq: z.number().int(), text: z.string() }).strict()), + /** Pass back to read the following lines. */ + nextCursor: z.number().int().nonnegative(), + truncated: z.boolean(), + /** The build is terminal: no more lines will arrive. */ + complete: z.boolean(), + }) + .strict(); +export type EnvironmentBuildLogs = z.infer; + +export const environmentBuildRequestResultSchema = z.discriminatedUnion('status', [ + z + .object({ + status: z.literal('accepted'), + buildId: z.string().uuid(), + /** False when the same request id or revision returned an existing build. */ + created: z.boolean(), + }) + .strict(), + z + .object({ + status: z.literal('conflict'), + reason: z.enum(['build_in_progress', 'revision_changed', 'cleanup_pending']), + buildId: z.string().uuid().nullable(), + }) + .strict(), + z + .object({ + status: z.literal('rejected'), + reason: z.enum([ + 'not_found', + 'unsupported_provider', + 'billing_blocked', + 'container_limit_reached', + 'repository_unauthorized', + ]), + }) + .strict(), +]); +export type EnvironmentBuildRequestResult = z.infer; + +export const environmentBuildCancelResultSchema = z + .object({ status: z.enum(['canceling', 'canceled', 'not_active']) }) + .strict(); +export type EnvironmentBuildCancelResult = z.infer; diff --git a/services/cloud-agent-next/src/shared/environment-snapshot-name.test.ts b/services/cloud-agent-next/src/shared/environment-snapshot-name.test.ts new file mode 100644 index 00000000000..392ce0af2b9 --- /dev/null +++ b/services/cloud-agent-next/src/shared/environment-snapshot-name.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; +import { environmentSnapshotName } from './environment-snapshot-name.js'; + +describe('environmentSnapshotName', () => { + it('keeps names in UTC across date boundaries and distinguishes same-minute builds', () => { + expect(environmentSnapshotName(Date.parse('2026-10-25T00:01:02+02:00'))).toBe( + '24 Oct 2026, 22:01:02 UTC' + ); + expect(environmentSnapshotName(Date.parse('2026-10-24T22:01:03Z'))).toBe( + '24 Oct 2026, 22:01:03 UTC' + ); + }); +}); diff --git a/services/cloud-agent-next/src/shared/environment-snapshot-name.ts b/services/cloud-agent-next/src/shared/environment-snapshot-name.ts new file mode 100644 index 00000000000..d20b7443546 --- /dev/null +++ b/services/cloud-agent-next/src/shared/environment-snapshot-name.ts @@ -0,0 +1,14 @@ +/** Stable display name shared by settings and session startup, independent of viewer timezone. */ +export function environmentSnapshotName(capturedAt: number): string { + return new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hourCycle: 'h23', + timeZone: 'UTC', + timeZoneName: 'short', + }).format(capturedAt); +} diff --git a/services/cloud-agent-next/src/shared/protocol.ts b/services/cloud-agent-next/src/shared/protocol.ts index c46e2d67217..3f1570ae0f2 100644 --- a/services/cloud-agent-next/src/shared/protocol.ts +++ b/services/cloud-agent-next/src/shared/protocol.ts @@ -139,6 +139,7 @@ export type PreparingStep = | 'workspace_restore' | 'workspace_backup' | 'sandbox_provision' + | 'environment_snapshot' | 'sandbox_boot' | 'snapshot_fallback' | 'kilo_server' diff --git a/services/cloud-agent-next/src/shared/runtime-distribution.ts b/services/cloud-agent-next/src/shared/runtime-distribution.ts index 9d631c48af6..8b341424e31 100644 --- a/services/cloud-agent-next/src/shared/runtime-distribution.ts +++ b/services/cloud-agent-next/src/shared/runtime-distribution.ts @@ -38,8 +38,21 @@ export const WRAPPER_BUNDLES: readonly WrapperBundle[] = [ installName: 'kilocode-control-plane-wrapper.js', executable: false, }, + { + entry: 'src/environment-builder/main.ts', + distName: 'environment-builder.js', + installName: 'kilocode-environment-builder.js', + executable: false, + }, ]; +/** + * The Bun binary that runs a bundle as a container's main process. It is an + * absolute path so the main-process identity check can match `/proc/1/cmdline` + * exactly; both images install Bun there. + */ +export const RUNTIME_BUN_PATH = '/usr/local/bin/bun'; + export const CONTROL_PLANE_SUPERVISOR = { wrapperPath: 'control-plane-supervisor.sh', installName: 'kilocode-control-plane-supervisor.sh', diff --git a/services/cloud-agent-next/src/shared/runtime-environment.ts b/services/cloud-agent-next/src/shared/runtime-environment.ts index 872ec550225..ec85b61780e 100644 --- a/services/cloud-agent-next/src/shared/runtime-environment.ts +++ b/services/cloud-agent-next/src/shared/runtime-environment.ts @@ -57,3 +57,19 @@ export const CONTROL_RUNTIME_RESERVED_ENV_VARS = [ * write-time rejection because it cannot import from this service. */ export const RESERVED_ENV_VAR_PREFIXES = ['XDG_', 'KILO', 'OPENCODE'] as const; + +/** + * Root of the directory-keyed homes (plan "Builder and session home"). A + * `worktree` runtime allocation keys HOME by directory alone under this root, + * so an environment builder and every session of an isolated sandbox share the + * one home of `/workspace/app`, and caches its setup wrote there are reused. + * `/tmp` is not used: setup scripts clear it and `TMPDIR` would move it. + */ +export const CONTROL_HOME_ROOT = '/home'; + +/** + * Directory name, under the OS temporary directory, of the per-scope homes of + * allocations that predate directory-keyed homes. Those allocations keep it for + * their lifetime; the log uploader reads both roots while they exist. + */ +export const LEGACY_CONTROL_HOME_DIR_NAME = 'kilo-worktrees'; diff --git a/services/cloud-agent-next/src/types.ts b/services/cloud-agent-next/src/types.ts index cfaa915b36f..fa832779763 100644 --- a/services/cloud-agent-next/src/types.ts +++ b/services/cloud-agent-next/src/types.ts @@ -1,4 +1,5 @@ import type { SessionId } from './types/ids.js'; +import type { EnvironmentBuild } from './environment-build/environment-build-do.js'; import type { getSandbox, ExecutionSession, Sandbox } from '@cloudflare/sandbox'; import type { CloudAgentQueueReport } from '@kilocode/worker-utils/cloud-agent-queue-report'; import type { CloudAgentAuthority } from '@kilocode/worker-utils/cloud-agent-authority'; @@ -542,6 +543,8 @@ export type Env = { SANDBOX_SESSION: DurableObjectNamespace; /** Durable Object namespace for DO-managed Cloudflare containers (control-plane only) */ SANDBOX_CONTAINERS: DurableObjectNamespace; + /** One environment build coordinator per repository environment id. */ + ENVIRONMENT_BUILD: DurableObjectNamespace; /** Durable Object namespace for per-user Kilo SDK facade coordination */ USER_KILO_FACADE: DurableObjectNamespace; /** Durable Object namespace for one-time stream/terminal ticket nonce consumption */ @@ -550,8 +553,6 @@ export type Env = { E2E_CALLBACK_SINK?: DurableObjectNamespace; /** One-way shared sandbox failover overrides keyed by shared identity */ SHARED_SANDBOX_OVERRIDES: KVNamespace; - /** Repository snapshot index for control-plane container starts; absent disables snapshots. */ - REPO_SNAPSHOTS?: KVNamespace; /** Service binding for the session ingest worker */ SESSION_INGEST: SessionIngestBinding; /** Record-only container lifecycle usage meter. */ @@ -623,8 +624,6 @@ export type Env = { /** Comma-separated user or org IDs allowed to pick a sandbox destination. `*` includes personal. */ SANDBOX_SELECTION_IDS?: string; CREDENTIAL_CONTAINMENT_ENABLED?: string; - /** Comma-separated user or org IDs whose isolated control-plane containers start from repository snapshots, or '*' for all */ - CONTAINER_REPO_SNAPSHOT_IDS?: string; /** * Wrapper-side tool/server memory cgroup partition configuration. See * MEMORY_CGROUPS_PLAN.md (W4). @@ -647,6 +646,8 @@ export type Env = { 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; + /** Explicit organization IDs allowed to build and restore environment snapshots. Empty is off. */ + ENVIRONMENT_SNAPSHOT_ORG_IDS?: string; /** R2 endpoint for S3-compatible API access (presigned URL generation) */ R2_ENDPOINT?: string; /** R2 read-only access key ID for downloading image attachments */ diff --git a/services/cloud-agent-next/test/e2e/deploy/README.md b/services/cloud-agent-next/test/e2e/deploy/README.md index 6ddf121ac03..4ade1e9868b 100644 --- a/services/cloud-agent-next/test/e2e/deploy/README.md +++ b/services/cloud-agent-next/test/e2e/deploy/README.md @@ -208,7 +208,6 @@ semantics. | `CREDENTIAL_CONTAINMENT_ENABLED=false` | Non-contained dispatch; see plan sections 5 and 11.6. | | `NEXTAUTH_SECRET` Secrets Store binding added | Verifies the ticket and API token, and seals runtime authorization. | | `SHARED_SANDBOX_OVERRIDES` KV binding pinned to the e2e namespace id | An id-less binding makes wrangler auto-provision the namespace and fail with `code: 10014` because the title already exists. | -| `REPO_SNAPSHOTS` KV binding removed | The e2e Worker has no `SandboxContainers` class, so no repository snapshots; an id-less binding would provision a namespace for nothing. | ### Container classes and removed bindings diff --git a/services/cloud-agent-next/test/integration/control-plane-session-v2.test.ts b/services/cloud-agent-next/test/integration/control-plane-session-v2.test.ts index 06d7212fc5a..acd3e3a6fcc 100644 --- a/services/cloud-agent-next/test/integration/control-plane-session-v2.test.ts +++ b/services/cloud-agent-next/test/integration/control-plane-session-v2.test.ts @@ -905,6 +905,31 @@ describe('SandboxSessionV2 message flow (fake sandbox peer)', () => { stream.close(); }); + it('keeps the environment snapshot detail after the sandbox starts', async () => { + const { sessionId, stub, peer } = await setup(); + peer.prepareView = peer.view('preparing'); + await stub.send(promptPayload('m1')); + const detail = 'Starting from the environment snapshot (revision 2)'; + await stub.onRoute({ state: 'preparing', step: 'sandbox_create', attemptId: peer.attemptId }); + await stub.onRoute({ + state: 'preparing', + step: 'environment_snapshot', + detail, + attemptId: peer.attemptId, + }); + await stub.onRoute({ state: 'preparing', step: 'sandbox_start', attemptId: peer.attemptId }); + + const stream = await connectStream(sessionId); + await waitForStreamEvent(stream, 'connected'); + const replayed = preparingRows(await drainStream(stream)); + expect( + replayed.find( + row => row.action === 'step_snapshot' && row.stepSnapshot?.key === 'environment_snapshot' + )?.stepSnapshot + ).toMatchObject({ status: 'completed', latestDetail: detail }); + stream.close(); + }); + it('preserves ordered phase occurrences and original starts across retries in one attempt', async () => { const { sessionId, stub, peer } = await setup(); const stream = await connectStream(sessionId); diff --git a/services/cloud-agent-next/test/integration/environment-build.test.ts b/services/cloud-agent-next/test/integration/environment-build.test.ts new file mode 100644 index 00000000000..7b95ac7a8ec --- /dev/null +++ b/services/cloud-agent-next/test/integration/environment-build.test.ts @@ -0,0 +1,823 @@ +import { env, reset, runInDurableObject } from 'cloudflare:test'; +import { afterEach, describe, expect, it } from 'vitest'; +import type { EnvironmentReference } from '@kilocode/cloud-agent-profile'; +import { + EnvironmentBuildCoordinator, + type CoordinatorDeps, + type RunnerAuth, +} from '../../src/environment-build/coordinator.js'; +import type { EnvironmentBuild } from '../../src/environment-build/environment-build-do.js'; +import type { + BuilderAllocation, + BuilderProvider, +} from '../../src/environment-build/builder-provider.js'; +import type { BuildStore } from '../../src/environment-build/store.js'; +import type { NativeCaptureResult } from '../../src/sandbox-containers/native-capture.js'; +import { MAX_BUILD_LOG_BYTES } from '../../src/environment-build/policy.js'; +import { isEnvironmentSnapshotsEnabled } from '../../src/environment-build/enrollment.js'; + +const OWNER = { type: 'organization' as const, id: '22222222-2222-4222-8222-222222222222' }; +const REPOSITORY = { platform: 'github' as const, repoFullName: 'acme/widgets' }; + +type Namespace = DurableObjectNamespace; +const namespace = (env as unknown as { ENVIRONMENT_BUILD: Namespace }).ENVIRONMENT_BUILD; + +type Harness = { + /** Each test gets its own coordinator: `reset()` wipes storage under a reused instance. */ + environmentId: string; + stub: DurableObjectStub; + environment: { revision: number; deleted: boolean; access: 'manage' | 'read' | 'none' }; + launches: Array<{ allocation: BuilderAllocation; runner: { url: string; credential: string } }>; + captures: Array<{ captureId: string }>; + snapshotsTaken: Map; + stops: BuilderAllocation[]; + captureResult: (captureId: string) => NativeCaptureResult; + stopResult: 'terminal' | 'retryable'; + launchError: Error | null; + /** The image a container would start from now; null when it cannot be read. */ + currentImage: string | null; + alarms: number[]; + gitUnavailable?: boolean; + billingAdmission?: (allocation: BuilderAllocation) => Promise; + snapshotOrgIds: string; +}; + +function reference(harness: Harness): EnvironmentReference { + return { + environmentId: harness.environmentId, + owner: OWNER, + configurationRevision: harness.environment.revision, + repository: REPOSITORY, + }; +} + +/** Install a coordinator whose environment, provider and credentials are in-memory fakes. */ +async function setup(): Promise { + const environmentId = crypto.randomUUID(); + const stub = namespace.getByName(environmentId); + const harness: Harness = { + environmentId, + stub, + environment: { revision: 1, deleted: false, access: 'manage' }, + launches: [], + captures: [], + snapshotsTaken: new Map(), + stops: [], + captureResult: captureId => ({ + status: 'stored', + snapshotId: `snap-${captureId}`, + size: 42, + capturedAt: Date.now(), + baseImage: 'registry.example/app:1', + durationMs: 10, + }), + stopResult: 'terminal', + launchError: null, + currentImage: 'registry.example/app:1', + alarms: [], + snapshotOrgIds: OWNER.id, + }; + const provider: BuilderProvider = { + admitBilling: async allocation => { + await harness.billingAdmission?.(allocation); + }, + launch: async (allocation, launch) => { + if (harness.launchError) throw harness.launchError; + harness.launches.push({ allocation, runner: launch.runner }); + }, + capture: async (_allocation, captureId) => { + harness.captures.push({ captureId }); + // A capture id is taken once; a repeated call returns the stored outcome. + harness.snapshotsTaken.set(captureId, 1); + return harness.captureResult(captureId); + }, + stop: async allocation => { + harness.stops.push(allocation); + return harness.stopResult; + }, + renewLease: async () => undefined, + runtimeReleaseId: async () => null, + currentImage: async () => harness.currentImage, + }; + await runInDurableObject(stub, async (instance: EnvironmentBuild, state) => { + const host = instance as unknown as { + initialized: Promise; + store: BuildStore; + coordinator?: EnvironmentBuildCoordinator; + }; + await host.initialized; + const deps: CoordinatorDeps = { + now: () => Date.now(), + uuid: () => crypto.randomUUID(), + store: host.store, + transaction: write => state.storage.transactionSync(write), + // Alarms are driven by `tick`, so the runtime's own alarm never races a test. + armAlarm: async at => { + harness.alarms.push(at); + }, + environment: { + reference: async () => (harness.environment.deleted ? null : reference(harness)), + buildInput: async () => + harness.environment.deleted + ? null + : { + reference: reference(harness), + repoFullName: 'acme/widgets', + commands: ['pnpm install'], + plainVars: { PLAIN: 'value' }, + encryptedSecrets: { + TOKEN: { + encryptedData: 'x', + encryptedDEK: 'y', + algorithm: 'rsa-aes-256-gcm', + version: 1, + }, + }, + }, + actorAccess: async () => harness.environment.access, + gitSource: async () => + harness.gitUnavailable + ? { ok: false, reason: 'unavailable' } + : { + ok: true, + git: { + url: 'https://github.com/acme/widgets.git', + token: 'gh-token', + platform: 'github', + }, + }, + capacity: async () => 'ok', + }, + provider, + runnerUrl: (environment, build) => + `https://worker.test/environment-build/${environment}/${build}`, + runnerCredential: async claims => `cred:${claims.buildId}:${claims.allocationId}`, + runnerCredentialMatches: async (claims, credential) => + credential === `cred:${claims.buildId}:${claims.allocationId}`, + decryptSecrets: () => ({ TOKEN: 'secret-value' }), + billingInput: ({ sandboxId, buildId }) => ({ + sandboxId: sandboxId as never, + subject: { type: 'org', id: OWNER.id }, + actor: { type: 'user', id: 'user-1' }, + sessionId: `environment-build:${buildId}`, + metadata: { origin: 'environment-build' }, + }), + builderSandboxId: 'sbx-' + '0'.repeat(48), + unsupportedReason: owner => + isEnvironmentSnapshotsEnabled( + { ENVIRONMENT_SNAPSHOT_ORG_IDS: harness.snapshotOrgIds }, + owner + ) + ? null + : 'Snapshots are not enabled for this organization', + log: () => undefined, + }; + host.coordinator = new EnvironmentBuildCoordinator(environmentId, deps); + await state.storage.deleteAlarm(); + }); + return harness; +} + +async function manualBuild(harness: Harness, requestId = crypto.randomUUID()) { + return harness.stub.requestBuild({ + environmentId: harness.environmentId, + owner: OWNER, + expectedRevision: harness.environment.revision, + requestId, + trigger: 'manual', + actorUserId: 'user-1', + }); +} + +async function status(harness: Harness) { + return harness.stub.status({ + environmentId: harness.environmentId, + owner: OWNER, + configurationRevision: harness.environment.revision, + }); +} + +function runnerAuth(harness: Harness, buildId: string): RunnerAuth { + const launch = harness.launches.at(-1); + if (!launch) throw new Error('builder was not launched'); + return { + environmentId: harness.environmentId, + buildId, + allocationId: launch.allocation.allocationId, + credential: `cred:${buildId}:${launch.allocation.allocationId}`, + }; +} + +/** One alarm pass of the coordinator, as the runtime would run it. */ +async function tick(harness: Harness): Promise { + await runInDurableObject(harness.stub, async (instance: EnvironmentBuild) => { + await (instance as unknown as { coordinator: EnvironmentBuildCoordinator }).coordinator.alarm(); + }); +} + +/** Run alarm passes until the build stops moving or `limit` passes elapse. */ +async function drive(harness: Harness, limit = 6): Promise { + for (let pass = 0; pass < limit; pass += 1) { + const before = JSON.stringify(await status(harness)); + await tick(harness); + if (JSON.stringify(await status(harness)) === before) return; + } +} + +/** Admit a manual build and run it through the builder's ready report. */ +async function readyBuild(harness: Harness) { + const accepted = await manualBuild(harness); + if (accepted.status !== 'accepted') throw new Error(`build not accepted: ${accepted.status}`); + await tick(harness); + const auth = runnerAuth(harness, accepted.buildId); + return { buildId: accepted.buildId, auth }; +} + +afterEach(async () => { + await reset(); +}); + +describe('environment build lifecycle', () => { + it('settles a billing admission when cancellation wins before launch', async () => { + const h = await setup(); + const accepted = await manualBuild(h); + if (accepted.status !== 'accepted') throw new Error('not accepted'); + const admitted: BuilderAllocation[] = []; + await runInDurableObject(h.stub, (instance: EnvironmentBuild) => { + const coordinator = (instance as unknown as { coordinator: EnvironmentBuildCoordinator }) + .coordinator; + h.billingAdmission = async allocation => { + admitted.push(allocation); + await coordinator.cancel(accepted.buildId); + }; + }); + await tick(h); + await drive(h); + expect(admitted).toHaveLength(1); + expect(h.launches).toHaveLength(0); + expect(h.stops).toEqual(admitted); + expect((await status(h)).latestBuild?.state).toBe('canceled'); + }); + + it('delays queued retries while the repository token source is unavailable', async () => { + const h = await setup(); + await manualBuild(h); + h.gitUnavailable = true; + const before = Date.now(); + await tick(h); + expect((await status(h)).activeBuild?.state).toBe('queued'); + expect(h.alarms.at(-1)).toBeGreaterThanOrEqual(before + 30_000); + h.gitUnavailable = false; + await tick(h); + expect(h.launches).toHaveLength(1); + }); + + it('returns recent log output when no page cursor is supplied', async () => { + const h = await setup(); + const { buildId, auth } = await readyBuild(h); + await h.stub.runnerEvents(auth, { + sequence: 0, + logs: Array.from({ length: 512 }, (_, i) => `line ${i}`), + }); + await h.stub.runnerEvents(auth, { sequence: 1, logs: ['failure detail at the end'] }); + const tail = await h.stub.logs({ environmentId: h.environmentId, buildId, limit: 500 }); + expect(tail?.lines).toHaveLength(500); + expect(tail?.lines.at(-1)?.text).toBe('failure detail at the end'); + const first = await h.stub.logs({ + environmentId: h.environmentId, + buildId, + cursor: 0, + limit: 10, + }); + expect(first?.lines[0]?.text).toBe('line 0'); + }); + + it('builds without a chat, publishes the snapshot and stops the exact builder', async () => { + const harness = await setup(); + const { buildId, auth } = await readyBuild(harness); + + expect(harness.launches).toHaveLength(1); + expect(harness.launches[0]?.runner).toEqual({ + url: `https://worker.test/environment-build/${harness.environmentId}/${buildId}`, + credential: auth.credential, + }); + expect(harness.launches[0]?.allocation.instance).toBe('standard-4'); + + const input = await harness.stub.runnerInput(auth); + expect(input).toMatchObject({ + buildId, + directory: '/workspace/app', + commands: ['pnpm install'], + env: { PLAIN: 'value', TOKEN: 'secret-value' }, + secretKeys: ['TOKEN'], + git: { url: 'https://github.com/acme/widgets.git', token: 'gh-token' }, + }); + await expect( + harness.stub.runnerEvents(auth, { sequence: 0, step: 'setup', logs: ['installing'] }) + ).resolves.toEqual({ action: 'continue' }); + await expect( + harness.stub.runnerResult(auth, { + outcome: 'ready', + commit: 'a'.repeat(40), + branch: 'main', + setupDurationMs: 1234, + }) + ).resolves.toBe('ok'); + await drive(harness); + + const done = await status(harness); + expect(done.activeBuild).toBeNull(); + expect(done.latestBuild).toMatchObject({ + buildId, + state: 'succeeded', + sourceCommit: 'a'.repeat(40), + setupDurationMs: 1234, + cleanup: 'confirmed', + }); + expect(done.published).toMatchObject({ buildId, revision: 1, stale: null, size: 42 }); + expect(harness.stops.map(stop => stop.allocationId)).toEqual([auth.allocationId]); + expect(harness.captures).toHaveLength(1); + const logs = await harness.stub.logs({ + environmentId: harness.environmentId, + buildId, + cursor: 0, + limit: 10, + }); + expect(logs).toMatchObject({ lines: [{ seq: 1, text: 'installing' }], complete: true }); + // Secrets are not readable once the allocation is gone. + await expect(harness.stub.runnerInput(auth)).resolves.toEqual({ refused: 'gone' }); + }); + + it('deduplicates requests and refuses a second revision while a build runs', async () => { + const harness = await setup(); + const requestId = crypto.randomUUID(); + const first = await manualBuild(harness, requestId); + expect(first).toMatchObject({ status: 'accepted', created: true }); + await expect(manualBuild(harness, requestId)).resolves.toEqual({ + ...first, + created: false, + }); + await expect(manualBuild(harness)).resolves.toMatchObject({ + status: 'accepted', + created: false, + }); + + harness.environment.revision = 2; + await expect(manualBuild(harness)).resolves.toMatchObject({ + status: 'conflict', + reason: 'build_in_progress', + }); + // A stale expected revision is refused before any build is created. + await runInDurableObject(harness.stub, async () => undefined); + }); + + it('applies a replayed event batch once and ignores a replayed result', async () => { + const harness = await setup(); + const { buildId, auth } = await readyBuild(harness); + await harness.stub.runnerEvents(auth, { sequence: 3, logs: ['one'] }); + await harness.stub.runnerEvents(auth, { sequence: 3, logs: ['one'] }); + await harness.stub.runnerEvents(auth, { sequence: 4, logs: ['two'] }); + const ready = { + outcome: 'ready' as const, + commit: 'b'.repeat(40), + branch: 'main', + setupDurationMs: 1, + }; + await harness.stub.runnerResult(auth, ready); + await harness.stub.runnerResult(auth, ready); + await drive(harness); + + const logs = await harness.stub.logs({ + environmentId: harness.environmentId, + buildId, + cursor: 0, + limit: 10, + }); + expect(logs?.lines.map(line => line.text)).toEqual(['one', 'two']); + expect(harness.captures).toHaveLength(1); + }); + + it('refuses a builder with another credential or allocation', async () => { + const harness = await setup(); + const { auth } = await readyBuild(harness); + + await expect(harness.stub.runnerInput({ ...auth, credential: 'forged' })).resolves.toEqual({ + refused: 'unauthorized', + }); + await expect( + harness.stub.runnerEvents({ ...auth, allocationId: crypto.randomUUID() }, { sequence: 0 }) + ).resolves.toEqual({ refused: 'unauthorized' }); + }); + + it('reads a lost capture reply back by its capture id and never snapshots twice', async () => { + const harness = await setup(); + const { auth } = await readyBuild(harness); + await harness.stub.runnerResult(auth, { + outcome: 'ready', + commit: 'c'.repeat(40), + branch: 'main', + setupDurationMs: 1, + }); + // The first capture reply is lost after the platform stored the result. + let lost = true; + const stored = harness.captureResult; + harness.captureResult = captureId => { + if (lost) { + lost = false; + throw new Error('reply lost'); + } + return stored(captureId); + }; + await tick(harness); + expect((await status(harness)).activeBuild?.state).toBe('capturing'); + await drive(harness); + + expect((await status(harness)).published).not.toBeNull(); + const ids = new Set(harness.captures.map(capture => capture.captureId)); + expect(ids.size).toBe(1); + }); + + it('fails an uncertain capture without replacing the published snapshot', async () => { + const harness = await setup(); + const first = await readyBuild(harness); + await harness.stub.runnerResult(first.auth, { + outcome: 'ready', + commit: 'd'.repeat(40), + branch: 'main', + setupDurationMs: 1, + }); + await drive(harness); + const published = (await status(harness)).published; + + harness.captureResult = () => ({ status: 'failed', reason: 'uncertain' }); + const second = await readyBuild(harness); + await harness.stub.runnerResult(second.auth, { + outcome: 'ready', + commit: 'e'.repeat(40), + branch: 'main', + setupDurationMs: 1, + }); + await drive(harness); + + const after = await status(harness); + expect(after.latestBuild).toMatchObject({ + buildId: second.buildId, + state: 'failed', + failureReason: 'capture_uncertain', + }); + expect(after.published).toEqual(published); + }); + + it('makes a build obsolete when the configuration changes during it', async () => { + const harness = await setup(); + const { buildId, auth } = await readyBuild(harness); + await harness.stub.runnerResult(auth, { + outcome: 'ready', + commit: 'f'.repeat(40), + branch: 'main', + setupDurationMs: 1, + }); + harness.environment.revision = 2; + await drive(harness); + + const after = await status(harness); + expect(after.latestBuild).toMatchObject({ + buildId, + state: 'obsolete', + failureReason: 'configuration_changed', + }); + expect(after.published).toBeNull(); + expect(harness.stops).toHaveLength(1); + }); + + it('stops a build when the environment is deleted and refuses new admission', async () => { + const harness = await setup(); + const { buildId, auth } = await readyBuild(harness); + harness.environment.deleted = true; + await harness.stub.environmentDeleted({ environmentId: harness.environmentId }); + await drive(harness); + + const after = await status(harness); + expect(after.latestBuild).toMatchObject({ buildId, state: 'obsolete' }); + // A late builder result cannot publish into a deleted environment. + await expect( + harness.stub.runnerResult(auth, { + outcome: 'ready', + commit: 'a'.repeat(40), + branch: 'main', + setupDurationMs: 1, + }) + ).resolves.toEqual({ refused: 'gone' }); + expect((await status(harness)).published).toBeNull(); + await expect(manualBuild(harness)).resolves.toEqual({ + status: 'rejected', + reason: 'not_found', + }); + }); + + it('cancels the exact allocation and tells the builder to stop', async () => { + const harness = await setup(); + const { buildId, auth } = await readyBuild(harness); + + await expect( + harness.stub.cancel({ environmentId: harness.environmentId, buildId }) + ).resolves.toEqual({ status: 'canceling' }); + await expect(harness.stub.runnerEvents(auth, { sequence: 1 })).resolves.toEqual({ + action: 'cancel', + }); + await drive(harness); + + expect((await status(harness)).latestBuild).toMatchObject({ buildId, state: 'canceled' }); + expect(harness.stops.map(stop => stop.allocationId)).toEqual([auth.allocationId]); + }); + + it('fails a builder that stops reporting', async () => { + const harness = await setup(); + const { buildId, auth } = await readyBuild(harness); + await harness.stub.runnerEvents(auth, { sequence: 0 }); + await runInDurableObject(harness.stub, async (instance: EnvironmentBuild) => { + const host = instance as unknown as { store: BuildStore }; + host.store.update(buildId, { last_heartbeat_at: Date.now() - 120_000 }); + }); + await drive(harness); + + expect((await status(harness)).latestBuild).toMatchObject({ + buildId, + state: 'failed', + failureReason: 'builder_lost', + }); + }); + + it('blocks a new build until an unconfirmed stop is confirmed', async () => { + const harness = await setup(); + const { buildId, auth } = await readyBuild(harness); + harness.stopResult = 'retryable'; + await harness.stub.runnerResult(auth, { + outcome: 'failed', + step: 'setup', + reason: 'setup_failed', + }); + await tick(harness); + + expect((await status(harness)).activeBuild).toMatchObject({ + buildId, + state: 'stopping', + outcome: 'failed', + cleanup: 'pending', + }); + await expect(manualBuild(harness)).resolves.toMatchObject({ + status: 'conflict', + reason: 'cleanup_pending', + }); + harness.stopResult = 'terminal'; + await runInDurableObject(harness.stub, async (instance: EnvironmentBuild) => { + (instance as unknown as { store: BuildStore }).store.update(buildId, { next_cleanup_at: 0 }); + }); + await drive(harness); + expect((await status(harness)).latestBuild).toMatchObject({ buildId, state: 'failed' }); + }); + + it('bounds the stored log of one build', async () => { + const harness = await setup(); + const { buildId, auth } = await readyBuild(harness); + const line = 'x'.repeat(8_000); + let sequence = 0; + for (let written = 0; written <= MAX_BUILD_LOG_BYTES; written += line.length * 100) { + await harness.stub.runnerEvents(auth, { + sequence: sequence++, + logs: Array.from({ length: 100 }, () => line), + }); + } + const logs = await harness.stub.logs({ + environmentId: harness.environmentId, + buildId, + cursor: 0, + limit: 1000, + }); + expect(logs?.truncated).toBe(true); + expect(logs?.lines.at(-1)?.text).toContain('truncated'); + const bytes = logs?.lines.reduce((sum, entry) => sum + entry.text.length, 0) ?? 0; + expect(bytes).toBeLessThanOrEqual(MAX_BUILD_LOG_BYTES + 100); + }); +}); + +describe('environment snapshot resolution', () => { + const sessionReference = (harness: Harness, revision: number) => ({ + environmentId: harness.environmentId, + owner: OWNER, + configurationRevision: revision, + repository: REPOSITORY, + }); + + it('denies manual and automatic builds for an organization outside the allowlist', async () => { + const harness = await setup(); + harness.snapshotOrgIds = 'another-org'; + expect(await manualBuild(harness)).toEqual({ + status: 'rejected', + reason: 'unsupported_provider', + }); + expect( + await harness.stub.resolveSnapshot({ + reference: sessionReference(harness, 1), + actorUserId: 'user-1', + }) + ).toEqual({ + snapshot: null, + reason: 'unsupported', + admission: { status: 'skipped', reason: 'unsupported' }, + }); + expect(await status(harness)).toMatchObject({ + supported: false, + activeBuild: null, + published: null, + }); + expect(harness.launches).toHaveLength(0); + }); + + it('retains a published snapshot but refuses restore after organization access is removed', async () => { + const harness = await setup(); + const { auth } = await readyBuild(harness); + await harness.stub.runnerResult(auth, { + outcome: 'ready', + commit: 'a'.repeat(40), + branch: 'main', + setupDurationMs: 1, + }); + await drive(harness); + expect((await status(harness)).published).not.toBeNull(); + harness.snapshotOrgIds = ''; + expect( + await harness.stub.resolveSnapshot({ + reference: sessionReference(harness, 1), + actorUserId: 'user-1', + }) + ).toMatchObject({ + snapshot: null, + reason: 'unsupported', + admission: { status: 'skipped', reason: 'unsupported' }, + }); + expect(await status(harness)).toMatchObject({ + supported: false, + published: expect.any(Object), + }); + }); + + it('does not launch a queued build after organization access is removed', async () => { + const harness = await setup(); + await manualBuild(harness); + harness.snapshotOrgIds = ''; + await drive(harness); + expect(harness.launches).toHaveLength(0); + expect(await status(harness)).toMatchObject({ + activeBuild: null, + latestBuild: { state: 'obsolete', failureReason: 'snapshots_disabled' }, + }); + }); + + it('stops a running builder after organization access is removed', async () => { + const harness = await setup(); + await readyBuild(harness); + harness.snapshotOrgIds = ''; + await drive(harness); + expect(harness.stops).toHaveLength(1); + expect(harness.captures).toHaveLength(0); + expect(await status(harness)).toMatchObject({ + activeBuild: null, + latestBuild: { state: 'obsolete' }, + }); + }); + + it('admits one build however many sessions start without a snapshot', async () => { + const harness = await setup(); + const first = await harness.stub.resolveSnapshot({ + reference: sessionReference(harness, 1), + actorUserId: 'user-2', + }); + const second = await harness.stub.resolveSnapshot({ + reference: sessionReference(harness, 1), + actorUserId: 'user-3', + }); + + expect(first).toMatchObject({ + snapshot: null, + reason: 'missing', + admission: { status: 'admitted' }, + }); + expect(second).toMatchObject({ + snapshot: null, + admission: { status: 'active', buildId: (first.admission as { buildId: string }).buildId }, + }); + expect((await status(harness)).activeBuild).toMatchObject({ trigger: 'session', revision: 1 }); + }); + + it('reuses an older snapshot after an edit and admits the current revision', async () => { + const harness = await setup(); + const { auth } = await readyBuild(harness); + await harness.stub.runnerResult(auth, { + outcome: 'ready', + commit: 'a'.repeat(40), + branch: 'main', + setupDurationMs: 1, + }); + await drive(harness); + await expect( + harness.stub.resolveSnapshot({ + reference: sessionReference(harness, 1), + actorUserId: 'user-1', + }) + ).resolves.toMatchObject({ + reason: 'eligible', + snapshot: { revision: 1, stale: null }, + admission: { status: 'not_needed' }, + }); + + harness.environment.revision = 2; + const after = await harness.stub.resolveSnapshot({ + reference: sessionReference(harness, 2), + actorUserId: 'user-1', + }); + expect(after).toMatchObject({ + reason: 'eligible', + snapshot: { revision: 1, stale: 'revision', baseImage: 'registry.example/app:1' }, + admission: { status: 'admitted' }, + }); + // An older admitted session never gets a newer snapshot. + harness.environment.revision = 1; + }); + + it('skips a snapshot for another sandbox image and admits its replacement', async () => { + const harness = await setup(); + const { auth } = await readyBuild(harness); + await harness.stub.runnerResult(auth, { + outcome: 'ready', + commit: 'a'.repeat(40), + branch: 'main', + setupDurationMs: 1, + }); + await drive(harness); + + harness.currentImage = 'registry.example/app:2'; + await expect( + harness.stub.resolveSnapshot({ + reference: sessionReference(harness, 1), + actorUserId: 'user-1', + }) + ).resolves.toMatchObject({ + reason: 'incompatible', + snapshot: null, + admission: { status: 'admitted' }, + }); + const view = await status(harness); + expect(view.published).toMatchObject({ revision: 1, stale: 'image' }); + expect(view.activeBuild).toMatchObject({ trigger: 'session', revision: 1 }); + }); + + it('skips admission for a superseded session revision and for a deleted environment', async () => { + const harness = await setup(); + harness.environment.revision = 3; + await expect( + harness.stub.resolveSnapshot({ + reference: sessionReference(harness, 2), + actorUserId: 'user-1', + }) + ).resolves.toMatchObject({ admission: { status: 'skipped', reason: 'revision_superseded' } }); + + harness.environment.deleted = true; + await harness.stub.environmentDeleted({ environmentId: harness.environmentId }); + await expect( + harness.stub.resolveSnapshot({ + reference: sessionReference(harness, 3), + actorUserId: 'user-1', + }) + ).resolves.toMatchObject({ reason: 'deleted', admission: { status: 'skipped' } }); + }); + + it('backs off a failed automatic build while a manual build still admits', async () => { + const harness = await setup(); + const lookup = await harness.stub.resolveSnapshot({ + reference: sessionReference(harness, 1), + actorUserId: 'user-1', + }); + const buildId = (lookup.admission as { buildId: string }).buildId; + await tick(harness); + const auth = runnerAuth(harness, buildId); + await harness.stub.runnerResult(auth, { + outcome: 'failed', + step: 'setup', + reason: 'setup_failed', + }); + await drive(harness); + + await expect( + harness.stub.resolveSnapshot({ + reference: sessionReference(harness, 1), + actorUserId: 'user-1', + }) + ).resolves.toMatchObject({ admission: { status: 'skipped', reason: 'backoff' } }); + expect((await status(harness)).automaticBackoff).toMatchObject({ revision: 1, failures: 1 }); + await expect(manualBuild(harness)).resolves.toMatchObject({ + status: 'accepted', + created: true, + }); + }); +}); diff --git a/services/cloud-agent-next/test/integration/sandbox-control-v2-repo-snapshots.test.ts b/services/cloud-agent-next/test/integration/sandbox-control-v2-environment-snapshots.test.ts similarity index 64% rename from services/cloud-agent-next/test/integration/sandbox-control-v2-repo-snapshots.test.ts rename to services/cloud-agent-next/test/integration/sandbox-control-v2-environment-snapshots.test.ts index 701d9bc3278..5b405eed1c5 100644 --- a/services/cloud-agent-next/test/integration/sandbox-control-v2-repo-snapshots.test.ts +++ b/services/cloud-agent-next/test/integration/sandbox-control-v2-environment-snapshots.test.ts @@ -1,17 +1,16 @@ import { env, evictAllDurableObjects, reset, runInDurableObject } from 'cloudflare:test'; -import { eq } from 'drizzle-orm'; -import { drizzle } from 'drizzle-orm/durable-sqlite'; import { afterEach, describe, expect, it } from 'vitest'; -import { resolveSecret } from '../../src/auth.js'; import type { SandboxControlV2 } from '../../src/control-plane/sandbox/sandbox-do.js'; import type { SandboxSessionV2 } from '../../src/control-plane/session/session-do.js'; import { sessionDoName } from '../../src/session-identity.js'; -import { verifySandboxLaunchCredential } from '../../src/sandbox-control/credential.js'; -import { validateControlLogUploadGrant } from '../../src/sandbox-control/log-upload-grant.js'; import { allocation as allocationTable, routes as routesTable, } from '../../src/control-plane/sandbox/sqlite-schema.js'; +import { eq } from 'drizzle-orm'; +import { drizzle } from 'drizzle-orm/durable-sqlite'; +import type { SnapshotResolution } from '../../src/environment-build/coordinator.js'; +import type { SessionEnvironmentReference } from '../../src/environment-build/reference.js'; import type { ProviderAdapter, ProviderCreateIntent, @@ -24,6 +23,7 @@ import type { ControlPlaneRouteSpec } from '../../src/shared/control-plane-proto import { CONTROL_PLANE_TIMERS } from '../../src/shared/control-plane-timers.js'; import { createFakeCredentialBroker, + FAKE_RUNTIME_AUTHORIZED_KILO_TOKEN, FAKE_SANDBOX_CONTAINMENT_NAMESPACE, installFakeCredentialEnv, } from './helpers/fake-credentials.js'; @@ -37,22 +37,59 @@ import { waitFor } from './wait-for.js'; const SESSION = 'workspace_11111111-1111-1111-1111-111111111111'; const KILO_SESSION = 'ses_aaaaaaaaaaaaaaaaaaaaaaaaaa'; +const sessionIdentities = new Map(); const REPO_URL = 'https://github.com/acme/widgets.git'; const ISOLATED_DIRECTORY = '/workspace/app'; const LAUNCH_KEY = 'repository_launch'; -const ENROLLED = { CONTAINER_REPO_SNAPSHOT_IDS: '*' }; -const NOT_ENROLLED = { CONTAINER_REPO_SNAPSHOT_IDS: '' }; +const REFERENCE: SessionEnvironmentReference = { + environmentId: '44444444-4444-4444-8444-444444444444', + owner: { type: 'organization', id: '55555555-5555-4555-8555-555555555555' }, + configurationRevision: 2, + repository: { platform: 'github', repoFullName: 'acme/widgets' }, +}; +const SNAPSHOT = { snapshotId: 'snap-env', baseImage: 'registry.example/app:1' }; +const RESOLVED: SnapshotResolution = { + snapshot: { + ...SNAPSHOT, + revision: 1, + capturedAt: 1, + buildId: '66666666-6666-4666-8666-666666666666', + stale: 'revision', + }, + reason: 'eligible', + admission: { status: 'admitted', buildId: '77777777-7777-4777-8777-777777777777' }, +}; +const MISSING: SnapshotResolution = { + snapshot: null, + reason: 'missing', + admission: { status: 'active', buildId: '77777777-7777-4777-8777-777777777777' }, +}; + +/** A coordinator namespace that answers every lookup with one resolution and records it. */ +function fakeEnvironmentBuild(resolution: SnapshotResolution = RESOLVED) { + const lookups: unknown[] = []; + return { + lookups, + namespace: { + getByName: () => ({ + resolveSnapshot: async (input: unknown) => { + lookups.push(input); + return resolution; + }, + }), + }, + }; +} +const WITH_SNAPSHOT = { ENVIRONMENT_BUILD: fakeEnvironmentBuild().namespace }; +const WITHOUT_SNAPSHOT = { ENVIRONMENT_BUILD: fakeEnvironmentBuild(MISSING).namespace }; // The Durable Object shares one env object across tests, so every test states the env it // needs and the Worker secret a previous test removed is put back. const WORKER_SECRET = (env as unknown as { NEXTAUTH_SECRET: unknown }).NEXTAUTH_SECRET; -const WORKER_URL = (env as unknown as { WORKER_URL?: string }).WORKER_URL; type SandboxControlNamespace = DurableObjectNamespace; const sandboxNamespace = (env as unknown as { SANDBOX_CONTROL: SandboxControlNamespace }) .SANDBOX_CONTROL; -type Capture = { ref: string; repoKey: string; commit: string | undefined }; - type FakeProvider = { adapter: ProviderAdapter; launchEnvs: Record[]; @@ -60,9 +97,6 @@ type FakeProvider = { launchDeadlines: Array; launchRefs: string[]; stopRefs: Array; - captures: Capture[]; - captureResult: boolean | 'throw' | 'hang'; - releaseCapture: () => void; launchHold: boolean; releaseLaunch: () => void; stopHold: boolean; @@ -71,7 +105,7 @@ type FakeProvider = { function createFakeProvider( options: { - capture?: boolean; + snapshots?: boolean; startSource?: ProviderStartSource; holdLaunch?: boolean; holdStop?: boolean; @@ -81,7 +115,6 @@ function createFakeProvider( stopDelayMs?: number; } = {} ): FakeProvider { - let release: () => void = () => undefined; let releaseLaunch: () => void = () => undefined; let releaseStop: () => void = () => undefined; const provider: FakeProvider = { @@ -91,9 +124,6 @@ function createFakeProvider( launchDeadlines: [], launchRefs: [], stopRefs: [], - captures: [], - captureResult: true, - releaseCapture: () => release(), launchHold: options.holdLaunch ?? false, releaseLaunch: () => releaseLaunch(), stopHold: options.holdStop ?? false, @@ -140,21 +170,7 @@ function createFakeProvider( async logs() { return ''; }, - ...(options.capture === false - ? {} - : { - async captureRepository(ref: string, repoKey: string, commit?: string) { - provider.captures.push({ ref, repoKey, commit }); - if (provider.captureResult === 'throw') throw new Error('capture failed'); - if (provider.captureResult === 'hang') { - await new Promise(resolve => { - release = resolve; - }); - return true; - } - return provider.captureResult; - }, - }), + ...(options.snapshots === false ? {} : { environmentSnapshots: true as const }), }; return provider; } @@ -168,14 +184,13 @@ type RouteOverrides = { orgId?: string | null; sessionId?: string; kiloSessionId?: string; + environment?: SessionEnvironmentReference | null; }; -const preparedIdentities = new Map(); - function prepareInput(overrides: RouteOverrides = {}) { const sessionId = overrides.sessionId ?? SESSION; const kiloSessionId = overrides.kiloSessionId ?? KILO_SESSION; - preparedIdentities.set(sessionId, { userId: overrides.userId ?? 'user_123', kiloSessionId }); + sessionIdentities.set(sessionId, { userId: overrides.userId ?? 'user_123', kiloSessionId }); const git = overrides.git === null ? undefined : (overrides.git ?? { url: REPO_URL }); const spec: ControlPlaneRouteSpec = { sessionId, @@ -188,10 +203,11 @@ function prepareInput(overrides: RouteOverrides = {}) { }; return { spec, + ...(overrides.environment === null ? {} : { environment: overrides.environment ?? REFERENCE }), credentials: { userId: overrides.userId ?? 'user_123', kiloSessionId, - kiloToken: 'eyJhbGciOiJub25lIn0.eyJydW50aW1lQXV0aG9yaXphdGlvbiI6eyJpZCI6InJhXzEifX0.c2ln', + kiloToken: FAKE_RUNTIME_AUTHORIZED_KILO_TOKEN, orgId: overrides.orgId === null ? undefined : (overrides.orgId ?? 'org_123'), repository: { type: 'github' as const, repo: 'acme/widgets' }, scopeId: sessionId, @@ -202,22 +218,27 @@ function prepareInput(overrides: RouteOverrides = {}) { async function setup( sandboxId: string, provider: FakeProvider, - extraEnv: Record = ENROLLED, + extraEnv: Record = WITH_SNAPSHOT, peer = new FakeSessionPeer() ): Promise<{ stub: DurableObjectStub; peer: FakeSessionPeer }> { const stub = sandboxNamespace.getByName(sandboxId); - peer.runtimeProxyForSession = createRuntimeProxyMintingPeer(env, sessionId => { - const identity = preparedIdentities.get(sessionId); - if (!identity) throw new Error('Missing prepared identity'); - return identity; - }); await runInDurableObject(stub, async instance => { await instance.getAllocationState(); installFakeCredentialEnv(instance.env, createFakeCredentialBroker(), { NEXTAUTH_SECRET: WORKER_SECRET, - WORKER_URL, + // Credential grants require the runtime proxy facade, independent of local settings. + WORKER_URL: 'https://worker.example.com', + // These tests use fake providers, independently of local runtime deployment settings. + RUNTIME_STARTUP_MODE: 'preinstalled', + NATIVE_RUNTIME_IMAGE: 'all-in-one', + RUNTIME_RELEASE_DESCRIPTOR: undefined, ...extraEnv, }); + peer.runtimeProxyForSession = createRuntimeProxyMintingPeer(instance.env, sessionId => { + const identity = sessionIdentities.get(sessionId); + if (!identity) throw new Error('Missing session fixture identity'); + return identity; + }); Object.assign(instance, { createProviderAdapter: () => provider.adapter, provider: provider.adapter, @@ -227,14 +248,6 @@ async function setup( return { stub, peer }; } -function repoKeyOf(stub: DurableObjectStub, sessionId = SESSION) { - return runInDurableObject(stub, async (_instance, state) => { - const db = drizzle(state.storage, { logger: false }); - const rows = await db.select().from(routesTable).where(eq(routesTable.session_id, sessionId)); - return rows[0]?.repo_key ?? null; - }); -} - function readLaunchRecord(stub: DurableObjectStub) { return runInDurableObject(stub, (_instance, state) => state.storage.get(LAUNCH_KEY)); } @@ -254,17 +267,6 @@ async function connect(sandboxId: string, provider: FakeProvider, launchCount = return { wrapper, allocationId }; } -/** Releases a hung capture from inside the Durable Object that is awaiting it. */ -async function releaseCapture( - stub: DurableObjectStub, - provider: FakeProvider -): Promise { - await runInDurableObject(stub, async () => { - provider.releaseCapture(); - await new Promise(resolve => setTimeout(resolve, 25)); - }); -} - /** Releases a hung launch or stop from inside the Durable Object awaiting it. */ async function releaseHeld( stub: DurableObjectStub, @@ -286,150 +288,18 @@ afterEach(async () => { await reset(); }); -describe('repository snapshot key', () => { - it('names an eligible route and is stable across sandboxes, sessions and credential grants', async () => { - const first = createFakeProvider(); - const second = createFakeProvider(); - const { stub: a } = await setup('sbx__repo_key_a', first); - const { stub: b } = await setup('sbx__repo_key_b', second); - - await a.prepare(prepareInput({ env: { NPM_TOKEN: 'secret-1', A: '1' } })); - await b.prepare( - prepareInput({ - sessionId: 'workspace_22222222-2222-2222-2222-222222222222', - env: { A: '1', NPM_TOKEN: 'secret-1' }, - }) - ); - - const keyA = await repoKeyOf(a); - const keyB = await repoKeyOf(b, 'workspace_22222222-2222-2222-2222-222222222222'); - expect(keyA).toMatch(/^[0-9a-f]{64}$/); - expect(keyB).toBe(keyA); - }); - - it('changes with the user and the repository, not with env', async () => { - const keys = new Set(); - const variants: RouteOverrides[] = [ - {}, - { userId: 'user_other' }, - { git: { url: 'https://github.com/acme/other.git' } }, - ]; - for (const [index, variant] of variants.entries()) { - const { stub } = await setup(`sbx__repo_key_variant_${index}`, createFakeProvider()); - await stub.prepare(prepareInput(variant)); - keys.add(await repoKeyOf(stub)); - } - expect(keys.size).toBe(variants.length); - expect(keys.has(null)).toBe(false); - }); - - it('is the same for different env values', async () => { - const keys = new Set(); - for (const [index, env] of [{}, { FOO: 'bar' }, { FOO: 'baz' }].entries()) { - const { stub } = await setup(`sbx__repo_key_env_${index}`, createFakeProvider()); - await stub.prepare(prepareInput({ env })); - keys.add(await repoKeyOf(stub)); - } - expect(keys.size).toBe(1); - expect(keys.has(null)).toBe(false); - }); - +describe('environment snapshot launch', () => { it.each([ - ['a per-session directory', { directory: '/workspace/org/user/sessions/s1' }, undefined], - ['no repository', { git: null }, undefined], - ['an owner that is not enrolled', {}, NOT_ENROLLED], - ['no enrollment setting', {}, { CONTAINER_REPO_SNAPSHOT_IDS: undefined }], - ] as const)('gives no key for %s', async (_name, overrides, extraEnv) => { - const { stub } = await setup('sbx__repo_key_none', createFakeProvider(), extraEnv ?? ENROLLED); - await stub.prepare(prepareInput(overrides)); - expect(await repoKeyOf(stub)).toBeNull(); - }); - - it('gives no key when the provider cannot capture', async () => { - const { stub } = await setup('sbx__repo_key_nocap', createFakeProvider({ capture: false })); - await stub.prepare(prepareInput()); - expect(await repoKeyOf(stub)).toBeNull(); - }); - - it('gives no key when the Worker secret is unavailable', async () => { - const { stub } = await setup('sbx__repo_key_nosecret', createFakeProvider(), { - ...ENROLLED, - NEXTAUTH_SECRET: undefined, - }); - await stub.prepare(prepareInput()); - expect(await repoKeyOf(stub)).toBeNull(); - }); -}); - -describe('repository snapshot enrollment', () => { - it('enrolls a personal user by user ID and drives both capture and restore', async () => { - const provider = createFakeProvider({ startSource: 'repository' }); - const { stub } = await setup('sbx__repo_elig_user', provider, { - CONTAINER_REPO_SNAPSHOT_IDS: 'user_123', - }); - await stub.prepare(prepareInput({ orgId: null })); - const key = await repoKeyOf(stub); - expect(key).toMatch(/^[0-9a-f]{64}$/); - - const { wrapper } = await connect('sbx__repo_elig_user', provider); - - expect(provider.launchOptions).toEqual([{ repoKey: key }]); - const frame = await prepareFrameOf(wrapper); - expect(frame.spec.capture).toBe(true); - - wrapper.send({ type: 'workspace.capture', sessionId: SESSION, commit: 'abc123' }); - expect(await wrapper.next()).toEqual({ - type: 'workspace.captured', - sessionId: SESSION, - ok: true, - }); - expect(provider.captures).toEqual([ - { ref: expect.stringMatching(/^mem_/), repoKey: key, commit: 'abc123' }, - ]); - }); - - it('enrolls an org owner by org ID', async () => { - const { stub } = await setup('sbx__repo_elig_org', createFakeProvider(), { - CONTAINER_REPO_SNAPSHOT_IDS: 'org_123', - }); - await stub.prepare(prepareInput()); - expect(await repoKeyOf(stub)).toMatch(/^[0-9a-f]{64}$/); - }); - - it('gives no key and no capture to a non-matching personal owner', async () => { - const provider = createFakeProvider(); - const { stub } = await setup('sbx__repo_elig_none', provider, { - CONTAINER_REPO_SNAPSHOT_IDS: 'user_other,org_other', - }); - await stub.prepare(prepareInput({ orgId: null })); - expect(await repoKeyOf(stub)).toBeNull(); - - const { wrapper } = await connect('sbx__repo_elig_none', provider); - expect(provider.launchOptions).toEqual([{}]); - await prepareFrameOf(wrapper); - - wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - expect(await wrapper.next()).toEqual({ - type: 'workspace.captured', - sessionId: SESSION, - ok: false, - }); - expect(provider.captures).toEqual([]); - }); -}); - -describe('repository snapshot launch', () => { - it.each([ - { enrolled: true, skipped: false, budget: 300_000 }, - { enrolled: false, skipped: false, budget: 120_000 }, - { enrolled: true, skipped: true, budget: 120_000 }, + { environment: true, skipped: false, budget: 300_000 }, + { environment: false, skipped: false, budget: 120_000 }, + { environment: true, skipped: true, budget: 120_000 }, ])( - 'uses creation budget $budget for enrolled=$enrolled skipped=$skipped', - async ({ enrolled, skipped, budget }) => { - const sandboxId = `sbx__repo_create_budget_${enrolled}_${skipped}`; + 'uses creation budget $budget for environment=$environment skipped=$skipped', + async ({ environment, skipped, budget }) => { + const sandboxId = `sbx__repo_create_budget_${environment}_${skipped}`; const provider = createFakeProvider({ holdLaunch: true }); const { stub } = await setup(sandboxId, provider, { - ...(enrolled ? ENROLLED : NOT_ENROLLED), + ...WITH_SNAPSHOT, SANDBOX_CONTAINERS: FAKE_SANDBOX_CONTAINMENT_NAMESPACE, }); if (skipped) { @@ -441,7 +311,7 @@ describe('repository snapshot launch', () => { } const before = Date.now(); await stub.prepare({ - ...prepareInput(), + ...prepareInput(environment ? {} : { environment: null }), sandboxSelection: { provider: 'cloudflare-containers' }, }); await waitFor(() => expect(provider.launchEnvs).toHaveLength(1)); @@ -455,73 +325,39 @@ describe('repository snapshot launch', () => { ); it('cleans up a failed snapshot and launches the image on the same allocation and deadline', async () => { - const workerUrl = 'https://worker.example.test/'; const sandboxId = 'sbx__repo_launch_fallback'; const provider = createFakeProvider({ launches: [{ startSource: 'repository', snapshotFailed: true }, { startSource: 'image' }], }); const { stub } = await setup(sandboxId, provider, { - ...ENROLLED, + ...WITH_SNAPSHOT, SANDBOX_CONTAINERS: FAKE_SANDBOX_CONTAINMENT_NAMESPACE, - WORKER_URL: workerUrl, }); await stub.prepare({ ...prepareInput(), sandboxSelection: { provider: 'cloudflare-containers' }, }); - const key = await repoKeyOf(stub); await waitFor(() => expect(provider.launchEnvs).toHaveLength(2)); const launchDeadline = (await stub.getAllocationState()).createDeadlineAt; const { wrapper, allocationId } = await connect(sandboxId, provider, 2); - expect(provider.launchOptions).toEqual([ - { repoKey: key }, - { repoKey: key, skipRepository: true }, - ]); + expect(provider.launchOptions).toEqual([{ snapshot: SNAPSHOT }, {}]); expect(provider.launchRefs[1]).toBe(provider.launchRefs[0]); expect(provider.stopRefs).toEqual([provider.launchRefs[0]]); - const signingSecret = await runInDurableObject(stub, instance => - resolveSecret(instance.env.NEXTAUTH_SECRET) - ); - if (!signingSecret) throw new Error('Test signing secret unavailable'); - const launches = provider.launchEnvs.map( - ({ - SANDBOX_CONTROL_CREDENTIAL: credential, - CONTROL_LOG_UPLOAD_GRANT: uploadGrant, - CONTROL_WRAPPER_INSTANCE_ID: wrapperInstanceId, - CONTROL_LOG_UPLOAD_URL: uploadUrl, - ...stableEnv - }) => { - const launchClaims = verifySandboxLaunchCredential(credential, signingSecret); - if (!launchClaims) throw new Error('Invalid wrapper launch credential'); - expect({ - sandboxId: launchClaims.sandboxId, - allocationId: launchClaims.allocationId, - }).toEqual({ - sandboxId, - allocationId, - }); - expect(validateControlLogUploadGrant(`Bearer ${uploadGrant}`, signingSecret)).toEqual({ - sandboxId, - allocationId, - wrapperInstanceId, - }); - expect(uploadUrl).toBe( - `${workerUrl.replace(/\/$/, '')}/sandbox-logs/${sandboxId}/${allocationId}/${wrapperInstanceId}` - ); - return { credential, uploadGrant, wrapperInstanceId, uploadUrl, stableEnv }; - } - ); - expect(launches[1].stableEnv).toEqual(launches[0].stableEnv); - expect(launches[0].stableEnv).toMatchObject({ - CONTROL_PLANE_ALLOCATION_ID: allocationId, - PROVIDER_INSTANCE_ID: sandboxId, - CONTROL_SANDBOX_PROVIDER: 'cloudflare-containers', + // Diagnostic upload identities are per launch when a Worker URL is configured. + const withoutDiagnostics = (launchEnv: Record | undefined) => + Object.fromEntries( + Object.entries(launchEnv ?? {}).filter( + ([key]) => !key.startsWith('CONTROL_LOG_UPLOAD_') && key !== 'CONTROL_WRAPPER_INSTANCE_ID' + ) + ); + expect(withoutDiagnostics(provider.launchEnvs[1])).toEqual({ + ...withoutDiagnostics(provider.launchEnvs[0]), + SANDBOX_CONTROL_CREDENTIAL: expect.any(String), }); - expect(launches[1].credential !== launches[0].credential).toBe(true); - expect(launches[1].uploadGrant !== launches[0].uploadGrant).toBe(true); - expect(launches[1].wrapperInstanceId).not.toBe(launches[0].wrapperInstanceId); - expect(launches[1].uploadUrl).not.toBe(launches[0].uploadUrl); + expect(provider.launchEnvs[1].SANDBOX_CONTROL_CREDENTIAL).not.toBe( + provider.launchEnvs[0].SANDBOX_CONTROL_CREDENTIAL + ); expect(provider.launchDeadlines[0]).toEqual(expect.any(Number)); expect(provider.launchDeadlines[0]).toBe(launchDeadline); expect(provider.launchDeadlines[1]).toBe(launchDeadline); @@ -573,10 +409,9 @@ describe('repository snapshot launch', () => { confirmed: false, }); await stub.prepare(prepareInput()); - const key = await repoKeyOf(stub); await waitFor(() => expect(provider.launchEnvs).toHaveLength(1)); - expect(provider.launchOptions).toEqual([{ repoKey: key, skipRepository: true }]); + expect(provider.launchOptions).toEqual([{}]); await waitFor(() => expect(peer.routeUpdatesFor(SESSION)).toContainEqual({ state: 'preparing', @@ -677,7 +512,7 @@ describe('repository snapshot launch', () => { await waitFor(async () => expect((await stub.getAllocationState()).allocationId).not.toBe(allocationId) ); - expect(provider.launchOptions[1]).toMatchObject({ skipRepository: true }); + expect(provider.launchOptions[1]).toEqual({}); expect(provider.launchEnvs).toHaveLength(2); }); @@ -744,67 +579,91 @@ describe('repository snapshot launch', () => { expect(provider.launchEnvs).toHaveLength(1); }); - it('passes the route key to the provider launch and asks the wrapper to capture', async () => { + it('starts from the selected snapshot and never asks the wrapper to capture', async () => { + const lookups = fakeEnvironmentBuild(); const provider = createFakeProvider({ startSource: 'repository' }); - const { stub } = await setup('sbx__repo_launch', provider); - await stub.prepare(prepareInput()); - const key = await repoKeyOf(stub); + const { stub, peer } = await setup('sbx__env_launch_selected', provider, { + ENVIRONMENT_BUILD: lookups.namespace, + }); + await stub.prepare(prepareInput({ setupCommands: ['npm install'] })); + const { wrapper } = await connect('sbx__env_launch_selected', provider); + const frame = await prepareFrameOf(wrapper); - const { wrapper, allocationId } = await connect('sbx__repo_launch', provider); + expect(provider.launchOptions).toEqual([{ snapshot: SNAPSHOT }]); + expect(lookups.lookups).toEqual([{ reference: REFERENCE, actorUserId: 'user_123' }]); + expect(frame.spec.capture).toBeUndefined(); + expect(frame.spec).not.toHaveProperty('environment'); + await waitFor(() => + expect(peer.routeUpdatesFor(SESSION)).toContainEqual({ + state: 'preparing', + attemptId: expect.any(String), + step: 'environment_snapshot', + detail: + 'Using snapshot · 01 Jan 1970, 00:00:00 UTC; environment settings have changed; cached files may retain earlier values; a replacement build was started', + }) + ); + }); - expect(provider.launchOptions).toEqual([{ repoKey: key }]); - const frame = await prepareFrameOf(wrapper); - expect(frame.spec.capture).toBe(true); - expect(await readLaunchRecord(stub)).toEqual({ - allocationId, - startupMode: 'preinstalled', - startSource: 'repository', - confirmed: true, + it('starts from the image without a lookup when the session has no environment', async () => { + const lookups = fakeEnvironmentBuild(); + const provider = createFakeProvider(); + const { stub } = await setup('sbx__env_launch_none', provider, { + ENVIRONMENT_BUILD: lookups.namespace, }); + await stub.prepare(prepareInput({ environment: null })); + const { wrapper } = await connect('sbx__env_launch_none', provider); + const frame = await prepareFrameOf(wrapper); + + expect(provider.launchOptions).toEqual([{}]); + expect(lookups.lookups).toEqual([]); + expect(frame.spec.capture).toBeUndefined(); }); - it('launches without a key and never asks for a capture when no key applies', async () => { + it('starts from the image when no snapshot is eligible and reports the real build status', async () => { const provider = createFakeProvider(); - const { stub } = await setup('sbx__repo_launch_none', provider, NOT_ENROLLED); + const { stub, peer } = await setup('sbx__env_launch_missing', provider, WITHOUT_SNAPSHOT); await stub.prepare(prepareInput()); - - const { wrapper } = await connect('sbx__repo_launch_none', provider); + await connect('sbx__env_launch_missing', provider); expect(provider.launchOptions).toEqual([{}]); - const frame = await prepareFrameOf(wrapper); - expect(frame.spec.capture).toBeUndefined(); + await waitFor(() => + expect(peer.routeUpdatesFor(SESSION)).toContainEqual({ + state: 'preparing', + attemptId: expect.any(String), + step: 'environment_snapshot', + detail: 'Starting without an environment snapshot; a replacement build is running', + }) + ); }); - it('uses a key and asks the wrapper to capture when setup commands are present', async () => { - const provider = createFakeProvider({ startSource: 'repository' }); - const { stub } = await setup('sbx__repo_launch_setup', provider); - await stub.prepare(prepareInput({ setupCommands: ['pnpm install'] })); - const key = await repoKeyOf(stub); - expect(key).not.toBeNull(); - - const { wrapper } = await connect('sbx__repo_launch_setup', provider); + it('starts from the image for a shared worktree path and for a provider without snapshots', async () => { + const shared = createFakeProvider(); + const { stub: sharedStub } = await setup('sbx__env_launch_shared', shared); + await sharedStub.prepare(prepareInput({ directory: '/workspace/org/user/sessions/s1' })); + await waitFor(() => expect(shared.launchEnvs).toHaveLength(1)); + expect(shared.launchOptions).toEqual([{}]); - expect(provider.launchOptions).toEqual([{ repoKey: key }]); - const frame = await prepareFrameOf(wrapper); - expect(frame.spec.capture).toBe(true); + const unsupported = createFakeProvider({ snapshots: false }); + const { stub } = await setup('sbx__env_launch_unsupported', unsupported); + await stub.prepare(prepareInput()); + await waitFor(() => expect(unsupported.launchEnvs).toHaveLength(1)); + expect(unsupported.launchOptions).toEqual([{}]); }); - it('gives different keys to different setup commands for one repository', async () => { - const setupA = 'workspace_aaaa1111-1111-1111-1111-111111111111'; - const setupB = 'workspace_bbbb2222-2222-2222-2222-222222222222'; - const providerA = createFakeProvider(); - const { stub: stubA } = await setup('sbx__repo_setup_a', providerA); - await stubA.prepare(prepareInput({ sessionId: setupA, setupCommands: ['pnpm install'] })); - - const providerB = createFakeProvider(); - const { stub: stubB } = await setup('sbx__repo_setup_b', providerB); - await stubB.prepare(prepareInput({ sessionId: setupB, setupCommands: ['pnpm build'] })); - - const keyA = await repoKeyOf(stubA, setupA); - const keyB = await repoKeyOf(stubB, setupB); - expect(keyA).not.toBeNull(); - expect(keyB).not.toBeNull(); - expect(keyA).not.toBe(keyB); + it('answers a capture request from a pre-change wrapper at once without the provider', async () => { + const provider = createFakeProvider(); + const { stub } = await setup('sbx__env_capture_declined', provider); + await stub.prepare(prepareInput()); + const { wrapper } = await connect('sbx__env_capture_declined', provider); + await prepareFrameOf(wrapper); + + wrapper.send({ type: 'workspace.capture', sessionId: SESSION, commit: 'abc' }); + expect(await wrapper.next()).toEqual({ + type: 'workspace.captured', + sessionId: SESSION, + ok: false, + }); + expect(provider.stopRefs).toEqual([]); }); it('skips a snapshot whose start never connected and starts from the image', async () => { @@ -816,11 +675,10 @@ describe('repository snapshot launch', () => { confirmed: false, }); await stub.prepare(prepareInput()); - const key = await repoKeyOf(stub); await connect('sbx__repo_launch_discard', provider); - expect(provider.launchOptions).toEqual([{ repoKey: key, skipRepository: true }]); + expect(provider.launchOptions).toEqual([{}]); }); it.each([ @@ -831,11 +689,10 @@ describe('repository snapshot launch', () => { const { stub } = await setup('sbx__repo_launch_keep', provider); await putLaunchRecord(stub, { allocationId: 'previous-allocation', ...previous }); await stub.prepare(prepareInput()); - const key = await repoKeyOf(stub); await connect('sbx__repo_launch_keep', provider); - expect(provider.launchOptions).toEqual([{ repoKey: key }]); + expect(provider.launchOptions).toEqual([{ snapshot: SNAPSHOT }]); }); it('keeps the failed-snapshot skip when a launch fails before it completes', async () => { @@ -892,7 +749,6 @@ describe('repository snapshot launch', () => { }); const { stub } = await setup(sandboxId, provider); await stub.prepare(prepareInput()); - const key = await repoKeyOf(stub); const { allocationId } = await connect(sandboxId, provider); // The launch is still held when the wrapper connects. Stop the allocation @@ -902,7 +758,7 @@ describe('repository snapshot launch', () => { await db .update(allocationTable) .set({ last_frame_at: Date.now(), last_activity_at: Date.now() - 60 * 60_000 }) - .where(eq(allocationTable.id, 'current')); + .where(eq(allocationTable.allocation_id, allocationId)); await instance.alarm(); expect((await instance.getAllocationState()).kind).toBe('stopping'); }); @@ -919,7 +775,7 @@ describe('repository snapshot launch', () => { // Completing the stop starts the next allocation, which must keep the snapshot. await releaseHeld(stub, () => provider.releaseStop()); await waitFor(() => expect(provider.launchEnvs).toHaveLength(2)); - expect(provider.launchOptions[1]).toEqual({ repoKey: key }); + expect(provider.launchOptions[1]).toEqual({ snapshot: SNAPSHOT }); }); it('marks the launch confirmed only for the allocation whose wrapper connected', async () => { @@ -949,7 +805,7 @@ describe('repository snapshot launch', () => { }); }); -describe('repository snapshot preparation fallback', () => { +describe('environment snapshot preparation fallback', () => { it('keeps queued messages through fallback and delivers their original identities once', async () => { const sandboxId = 'sbx__repo_prepare_queued_messages'; const provider = createFakeProvider({ @@ -973,10 +829,10 @@ describe('repository snapshot preparation fallback', () => { sessionPeerFor: (ownerId: string, sessionId: string) => namespace.getByName(sessionDoName(ownerId, sessionId)), }); - installFakeRuntimeProxyPeer(instance, async () => ({ - userId: 'user_123', - kiloSessionId: KILO_SESSION, - })); + installFakeRuntimeProxyPeer( + instance, + async (_ownerId, sessionId) => sessionIdentities.get(sessionId) ?? null + ); }); for (const messageId of ['m1', 'm2']) { await session.send({ @@ -1035,7 +891,7 @@ describe('repository snapshot preparation fallback', () => { launches: [{ startSource: 'repository' }, { startSource: 'image' }], }); const peer = new FakeSessionPeer(); - const { stub } = await setup(sandboxId, provider, ENROLLED, peer); + const { stub } = await setup(sandboxId, provider, WITH_SNAPSHOT, peer); const input = prepareInput({ setupCommands: ['pnpm install'] }); const worktreeRecovery = { files: [{ path: 'changed.ts', status: 'modified' as const, patch: 'saved diff' }], @@ -1064,10 +920,7 @@ describe('repository snapshot preparation fallback', () => { const replacement = await prepareFrameOf(fresh.wrapper); expect(fresh.allocationId).not.toBe(allocationId); expect(provider.stopRefs).toEqual([provider.launchRefs[0]]); - expect(provider.launchOptions[1]).toEqual({ - repoKey: await repoKeyOf(stub), - skipRepository: true, - }); + expect(provider.launchOptions[1]).toEqual({}); expect(provider.launchEnvs[1].SANDBOX_CONTROL_CREDENTIAL).not.toBe( provider.launchEnvs[0].SANDBOX_CONTROL_CREDENTIAL ); @@ -1106,7 +959,7 @@ describe('repository snapshot preparation fallback', () => { launches: [{ startSource: 'repository' }, { startSource: 'image' }], }); const peer = new FakeSessionPeer(); - const { stub } = await setup(sandboxId, provider, ENROLLED, peer); + const { stub } = await setup(sandboxId, provider, WITH_SNAPSHOT, peer); await stub.prepare(prepareInput()); const { wrapper } = await connect(sandboxId, provider); await prepareFrameOf(wrapper); @@ -1177,7 +1030,7 @@ describe('repository snapshot preparation fallback', () => { if (startSource === 'repository') { const fresh = await connect(sandboxId, provider, 2); await prepareFrameOf(fresh.wrapper); - expect(provider.launchOptions[1]).toMatchObject({ skipRepository: true }); + expect(provider.launchOptions[1]).toEqual({}); } else { await waitFor(async () => expect((await stub.status({ sessionId: SESSION })).view.state).toBe('failed') @@ -1212,7 +1065,7 @@ describe('repository snapshot preparation fallback', () => { const sandboxId = 'sbx__repo_prepare_multiple_failures'; const provider = createFakeProvider({ holdLaunch: true }); const peer = new FakeSessionPeer(); - const { stub } = await setup(sandboxId, provider, ENROLLED, peer); + const { stub } = await setup(sandboxId, provider, WITH_SNAPSHOT, peer); await stub.prepare(prepareInput()); const { wrapper } = await connect(sandboxId, provider); await prepareFrameOf(wrapper); @@ -1303,7 +1156,7 @@ describe('repository snapshot preparation fallback', () => { const fresh = await connect(sandboxId, provider, 2); const frame = await prepareFrameOf(fresh.wrapper); expect(frame.spec.attemptId).toBe(view.attemptId); - expect(provider.launchOptions[1]).toMatchObject({ skipRepository: true }); + expect(provider.launchOptions[1]).toEqual({}); } else { expect((await reconstructed.status({ sessionId: SESSION })).view.state).toBe('failed'); expect(provider.launchEnvs).toHaveLength(1); @@ -1387,7 +1240,7 @@ describe('repository snapshot preparation fallback', () => { holdStop: true, }); const peer = new FakeSessionPeer(); - const { stub } = await setup(sandboxId, provider, ENROLLED, peer); + const { stub } = await setup(sandboxId, provider, WITH_SNAPSHOT, peer); await runInDurableObject(stub, instance => { Object.assign(instance, { sandboxTimers: () => ({ ...CONTROL_PLANE_TIMERS.sandbox, providerStopLadderMs: [] }), @@ -1448,254 +1301,3 @@ describe('repository snapshot preparation fallback', () => { } ); }); - -describe('repository capture request', () => { - async function preparing(provider: FakeProvider, sandboxId: string) { - const { stub } = await setup(sandboxId, provider); - await stub.prepare(prepareInput()); - const { wrapper, allocationId } = await connect(sandboxId, provider); - await prepareFrameOf(wrapper); - return { stub, wrapper, allocationId, key: await repoKeyOf(stub) }; - } - - it('captures through the provider with the route key and answers the wrapper', async () => { - const provider = createFakeProvider(); - const { wrapper, key } = await preparing(provider, 'sbx__repo_capture_ok'); - - wrapper.send({ type: 'workspace.capture', sessionId: SESSION, commit: 'abc123' }); - - expect(await wrapper.next()).toEqual({ - type: 'workspace.captured', - sessionId: SESSION, - ok: true, - }); - expect(provider.captures).toEqual([ - { ref: expect.stringMatching(/^mem_/), repoKey: key, commit: 'abc123' }, - ]); - }); - - it.each([ - ['the provider reports nothing saved', false], - ['the provider throws', 'throw'], - ] as const)('answers ok:false when %s', async (_name, result) => { - const provider = createFakeProvider(); - provider.captureResult = result; - const { wrapper } = await preparing(provider, `sbx__repo_capture_fail_${String(result)}`); - - wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - - expect(await wrapper.next()).toEqual({ - type: 'workspace.captured', - sessionId: SESSION, - ok: false, - }); - }); - - it('answers ok:false at once, without the provider, for a session with no key', async () => { - const provider = createFakeProvider(); - const { stub } = await setup('sbx__repo_capture_nokey', provider, NOT_ENROLLED); - await stub.prepare(prepareInput()); - const { wrapper } = await connect('sbx__repo_capture_nokey', provider); - await prepareFrameOf(wrapper); - - wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - - expect(await wrapper.next()).toEqual({ - type: 'workspace.captured', - sessionId: SESSION, - ok: false, - }); - expect(provider.captures).toEqual([]); - }); - - it.each([4 * 60_000, 9 * 60_000])( - 'skips capture with %s ms left and keeps the original preparation deadline', - async remainingMs => { - const provider = createFakeProvider(); - provider.captureResult = 'hang'; - const { stub, wrapper } = await preparing( - provider, - `sbx__repo_capture_budget_${remainingMs}` - ); - const deadlineAt = Date.now() + remainingMs; - await runInDurableObject(stub, async (_instance, state) => { - const db = drizzle(state.storage, { logger: false }); - await db - .update(routesTable) - .set({ attempt_deadline_at: deadlineAt }) - .where(eq(routesTable.session_id, SESSION)); - }); - - wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - - expect(await wrapper.next()).toEqual({ - type: 'workspace.captured', - sessionId: SESSION, - ok: false, - }); - expect(provider.captures).toEqual([]); - expect((await stub.status({ sessionId: SESSION })).view.state).toBe('preparing'); - await runInDurableObject(stub, async (_instance, state) => { - const db = drizzle(state.storage, { logger: false }); - const rows = await db.select().from(routesTable).where(eq(routesTable.session_id, SESSION)); - expect(rows[0]?.attempt_deadline_at).toBe(deadlineAt); - }); - - wrapper.send({ type: 'session.ready', sessionId: SESSION }); - await waitFor(async () => - expect((await stub.status({ sessionId: SESSION })).view.state).toBe('ready') - ); - } - ); - - it('answers ok:false for an unknown session and a route that is no longer preparing', async () => { - const provider = createFakeProvider(); - const { wrapper } = await preparing(provider, 'sbx__repo_capture_state'); - - wrapper.send({ type: 'workspace.capture', sessionId: 'workspace_unknown' }); - expect(await wrapper.next()).toMatchObject({ type: 'workspace.captured', ok: false }); - - wrapper.send({ type: 'session.ready', sessionId: SESSION }); - await waitFor(async () => expect(await readRouteState(SESSION)).toBe('ready')); - wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - expect(await wrapper.next()).toMatchObject({ type: 'workspace.captured', ok: false }); - expect(provider.captures).toEqual([]); - }); - - it('runs one capture for a request replayed while it is in flight', async () => { - const provider = createFakeProvider(); - provider.captureResult = 'hang'; - const { wrapper, stub } = await preparing(provider, 'sbx__repo_capture_replay'); - - wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - await waitFor(() => expect(provider.captures).toHaveLength(1)); - wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - await new Promise(resolve => setTimeout(resolve, 50)); - expect(provider.captures).toHaveLength(1); - - await releaseCapture(stub, provider); - expect(await wrapper.next()).toMatchObject({ type: 'workspace.captured', ok: true }); - }); - - it('ignores socket close during capture and resumes disconnect handling afterwards', async () => { - const provider = createFakeProvider(); - provider.captureResult = 'hang'; - const { wrapper, stub } = await preparing(provider, 'sbx__repo_capture_close'); - wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - await waitFor(() => expect(provider.captures).toHaveLength(1)); - - await runInDurableObject(stub, async (instance, state) => { - await instance.webSocketClose(state.getWebSockets()[0]); - expect((await instance.getAllocationState()).kind).toBe('connected'); - }); - await releaseCapture(stub, provider); - expect(await wrapper.next()).toMatchObject({ type: 'workspace.captured', ok: true }); - await runInDurableObject(stub, async (instance, state) => { - await instance.webSocketClose(state.getWebSockets()[0]); - expect((await instance.getAllocationState()).kind).toBe('disconnected'); - }); - }); - - it('keeps an overdue heartbeat socket alive only while capture runs', async () => { - const provider = createFakeProvider(); - provider.captureResult = 'hang'; - const { wrapper, stub } = await preparing(provider, 'sbx__repo_capture_heartbeat'); - wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - await waitFor(() => expect(provider.captures).toHaveLength(1)); - - await runInDurableObject(stub, async (instance, state) => { - const db = drizzle(state.storage, { logger: false }); - await db - .update(allocationTable) - .set({ last_frame_at: Date.now() - 60_000 }) - .where(eq(allocationTable.id, 'current')); - await instance.alarm(); - expect((await instance.getAllocationState()).kind).toBe('connected'); - expect(await state.storage.getAlarm()).toBeGreaterThan(Date.now()); - }); - await releaseCapture(stub, provider); - expect(await wrapper.next()).toMatchObject({ type: 'workspace.captured', ok: true }); - await runInDurableObject(stub, async instance => { - await instance.alarm(); - expect((await instance.getAllocationState()).kind).toBe('disconnected'); - }); - }); - - it.each([true, false])( - 'delivers capture across reconnect (completed first: %s)', - async completedFirst => { - const provider = createFakeProvider(); - provider.captureResult = 'hang'; - const sandboxId = `sbx__repo_capture_reply_reconnect_${completedFirst}`; - const { wrapper, stub } = await preparing(provider, sandboxId); - wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - await waitFor(() => expect(provider.captures).toHaveLength(1)); - wrapper.close(); - await wrapper.waitForClose(); - if (completedFirst) await releaseCapture(stub, provider); - - const { wrapper: reconnected } = await connect(sandboxId, provider); - if (!completedFirst) await releaseCapture(stub, provider); - expect(await reconnected.nextSkipping(['session.prepare'])).toEqual({ - type: 'workspace.captured', - sessionId: SESSION, - ok: true, - }); - expect(provider.captures).toHaveLength(1); - - reconnected.send({ type: 'session.ready', sessionId: SESSION }); - await waitFor(async () => - expect((await stub.status({ sessionId: SESSION })).view.state).toBe('ready') - ); - const { wrapper: readyReconnect } = await connect(sandboxId, provider); - expect(await readyReconnect.next(50)).toBeNull(); - } - ); - - it('does not replay a completed capture into a replacement allocation', async () => { - const provider = createFakeProvider(); - provider.captureResult = 'hang'; - const sandboxId = 'sbx__repo_capture_reply_replaced'; - const { wrapper, stub } = await preparing(provider, sandboxId); - wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - await waitFor(() => expect(provider.captures).toHaveLength(1)); - await releaseCapture(stub, provider); - expect(await wrapper.next()).toMatchObject({ type: 'workspace.captured', ok: true }); - - const replacement = crypto.randomUUID(); - await runInDurableObject(stub, async (_instance, state) => { - const db = drizzle(state.storage, { logger: false }); - await db - .update(allocationTable) - .set({ allocation_id: replacement }) - .where(eq(allocationTable.id, 'current')); - }); - provider.launchEnvs[0].CONTROL_PLANE_ALLOCATION_ID = replacement; - const { wrapper: replaced } = await connect(sandboxId, provider); - expect(await replaced.nextSkipping(['session.prepare'], 50)).toBeNull(); - }); - - it('does not block other frames while a capture runs', async () => { - const provider = createFakeProvider(); - provider.captureResult = 'hang'; - const { wrapper, stub } = await preparing(provider, 'sbx__repo_capture_async'); - - wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - await waitFor(() => expect(provider.captures).toHaveLength(1)); - wrapper.send({ type: 'session.ready', sessionId: SESSION }); - - await waitFor(async () => - expect((await stub.status({ sessionId: SESSION })).view.state).toBe('ready') - ); - await releaseCapture(stub, provider); - }); - - async function readRouteState(sessionId: string): Promise { - const stub = sandboxNamespace.getByName('sbx__repo_capture_state'); - return runInDurableObject(stub, async (_instance, state) => { - const db = drizzle(state.storage, { logger: false }); - const rows = await db.select().from(routesTable).where(eq(routesTable.session_id, sessionId)); - return rows[0]?.state; - }); - } -}); diff --git a/services/cloud-agent-next/test/integration/sandbox-control-v2-migrations.test.ts b/services/cloud-agent-next/test/integration/sandbox-control-v2-migrations.test.ts index 7d26eac1dfe..5e80aac0ddc 100644 --- a/services/cloud-agent-next/test/integration/sandbox-control-v2-migrations.test.ts +++ b/services/cloud-agent-next/test/integration/sandbox-control-v2-migrations.test.ts @@ -55,6 +55,7 @@ const EXPECTED_ROUTES_COLUMNS: ColumnSignature[] = [ { name: 'attempt_deadline_at', type: 'integer', notnull: 0, dflt_value: null, pk: 0 }, { name: 'attempt_id', type: 'text', notnull: 1, dflt_value: null, pk: 0 }, { name: 'credential_source', type: 'text', notnull: 0, dflt_value: null, pk: 0 }, + { name: 'environment', type: 'text', notnull: 0, dflt_value: null, pk: 0 }, { name: 'grant', type: 'text', notnull: 0, dflt_value: null, pk: 0 }, { name: 'reason', type: 'text', notnull: 0, dflt_value: null, pk: 0 }, { name: 'repo_key', type: 'text', notnull: 0, dflt_value: null, pk: 0 }, @@ -67,6 +68,7 @@ const EXPECTED_ROUTES_COLUMNS: ColumnSignature[] = [ const CONSOLIDATED_MIGRATION_AT = 1790455891314; const SCOPE_GRANTS_MIGRATION_AT = 1790793579352; const REPO_KEY_MIGRATION_AT = 1790860790832; +const ROUTE_ENVIRONMENT_MIGRATION_AT = 1792104724546; const OLD_FOUR_MIGRATIONS = { journal: { @@ -183,6 +185,7 @@ describe('sandbox control V2 consolidated migrations', () => { { hash: '', created_at: CONSOLIDATED_MIGRATION_AT }, { hash: '', created_at: SCOPE_GRANTS_MIGRATION_AT }, { hash: '', created_at: REPO_KEY_MIGRATION_AT }, + { hash: '', created_at: ROUTE_ENVIRONMENT_MIGRATION_AT }, ]); const applied = appliedMigrations(db); await migrate(db, migrations); @@ -222,13 +225,16 @@ describe('sandbox control V2 consolidated migrations', () => { expect(appliedMigrations(db).slice(oldApplied.length)).toEqual([ { hash: '', created_at: SCOPE_GRANTS_MIGRATION_AT }, { hash: '', created_at: REPO_KEY_MIGRATION_AT }, + { hash: '', created_at: ROUTE_ENVIRONMENT_MIGRATION_AT }, ]); expect(db.select().from(scopeGrants).all()).toEqual([]); expect(columnSignatures(db, 'allocation')).toEqual(oldAllocationColumns); expect(columnSignatures(db, 'routes')).toEqual(EXPECTED_ROUTES_COLUMNS); - expect(columnSignatures(db, 'routes').filter(column => column.name !== 'repo_key')).toEqual( - oldRoutesColumns - ); + expect( + columnSignatures(db, 'routes').filter( + column => column.name !== 'repo_key' && column.name !== 'environment' + ) + ).toEqual(oldRoutesColumns); expect(db.select().from(allocationTable).get()).toMatchObject({ id: 'current', state: 'stopped', @@ -241,6 +247,7 @@ describe('sandbox control V2 consolidated migrations', () => { grant: '{"fixture":"old-grant"}', credential_source: '{"fixture":"old-credential-source"}', repo_key: null, + environment: null, }); }); }); diff --git a/services/cloud-agent-next/test/integration/sandbox-runtime-startup.test.ts b/services/cloud-agent-next/test/integration/sandbox-runtime-startup.test.ts index 2a80ede6261..96de75bb213 100644 --- a/services/cloud-agent-next/test/integration/sandbox-runtime-startup.test.ts +++ b/services/cloud-agent-next/test/integration/sandbox-runtime-startup.test.ts @@ -95,7 +95,6 @@ function fakeProvider( options?: ProviderLaunchOptions; }[] = []; const releases: (() => void)[] = []; - const captures: string[] = []; const calls = { create: 0, stop: 0, observe: 0, lease: 0 }; const adapter: ProviderAdapter = { async ensureBillingAdmission() {}, @@ -128,12 +127,8 @@ function fakeProvider( async logs() { return ''; }, - async captureRepository(_ref, key) { - captures.push(key); - return true; - }, }; - return { adapter, launches, calls, captures, releases }; + return { adapter, launches, calls, releases }; } function configureHarness( @@ -144,7 +139,6 @@ function configureHarness( installFakeCredentialEnv(instance.env, createFakeCredentialBroker(), { NEXTAUTH_SECRET: workerSecret, WORKER_URL: 'https://worker.test', - CONTAINER_REPO_SNAPSHOT_IDS: '*', SANDBOX_CONTAINERS: FAKE_SANDBOX_CONTAINMENT_NAMESPACE, RUNTIME_STARTUP_MODE: 'downloaded', NATIVE_RUNTIME_IMAGE: 'all-in-one', @@ -225,13 +219,13 @@ afterEach(async () => { }); describe('downloaded startup in the allocation owner', () => { - it('retains the mode through recordLaunch and uses that mode for capture', async () => { + it('retains the downloaded mode through recordLaunch and refuses session capture', async () => { const h = await setup('sbx__runtime_mode'); await prepare(h); await waitFor(async () => expect((await h.stub.getAllocationState()).kind).toBe('starting')); expect((await record(h))?.startupMode).toBe('downloaded'); const startup = h.provider.launches[0]!; - expect(startup.options?.repoKey?.startsWith('downloaded:')).toBe(true); + expect(startup.options?.snapshot).toBeUndefined(); const wrapper = await FakeWrapper.connect({ sandboxId: h.id, credential: startup.env.SANDBOX_CONTROL_CREDENTIAL, @@ -241,9 +235,13 @@ describe('downloaded startup in the allocation owner', () => { allocationId: startup.env.CONTROL_PLANE_ALLOCATION_ID, }); await waitFor(async () => expect((await h.stub.getAllocationState()).kind).toBe('connected')); + expect((await wrapper.next())?.type).toBe('session.prepare'); wrapper.send({ type: 'workspace.capture', sessionId: SESSION }); - await waitFor(() => expect(h.provider.captures.length).toBe(1)); - expect(h.provider.captures[0]).toBe(startup.options?.repoKey); + expect(await wrapper.next()).toEqual({ + type: 'workspace.captured', + sessionId: SESSION, + ok: false, + }); expect(await report(h, 0, 'artifact_invalid')).toBe(false); }); it('stops immediately without image fallback when a permanent report wins', async () => { @@ -269,7 +267,7 @@ describe('downloaded startup in the allocation owner', () => { }); await prepare(h); await waitFor(() => expect(h.provider.launches.length).toBe(2)); - expect(h.provider.launches[1]?.options?.skipRepository).toBe(true); + expect(h.provider.launches[1]?.options?.snapshot).toBeUndefined(); expect(h.provider.launches[0]?.command === h.provider.launches[1]?.command).toBe(true); expect(await report(h, 0, 'artifact_invalid')).toBe(false); expect(await report(h, 1, 'artifact_invalid')).toBe(true); @@ -294,7 +292,7 @@ describe('downloaded startup in the allocation owner', () => { expect(await report(h, 0, 'base_incompatible')).toBe(true); if (holdFirst) await release(h, 0); await waitFor(() => expect(h.provider.launches.length).toBe(2)); - expect(h.provider.launches[1]?.options?.skipRepository).toBe(true); + expect(h.provider.launches[1]?.options?.snapshot).toBeUndefined(); expect(h.peer.routeUpdatesFor(SESSION).some(x => x.state === 'failed')).toBe(false); } ); diff --git a/services/cloud-agent-next/test/test-worker.ts b/services/cloud-agent-next/test/test-worker.ts index 0a98596e058..a53a326d6c3 100644 --- a/services/cloud-agent-next/test/test-worker.ts +++ b/services/cloud-agent-next/test/test-worker.ts @@ -75,6 +75,7 @@ export class UserKiloFacade extends RealUserKiloFacade { // The test Worker binds the production `SANDBOX_CONTROL`/`SANDBOX_SESSION` // names to the control-plane V2 classes, matching production after the C1c flip. export { SandboxControlV2, SandboxSessionV2 }; +export { EnvironmentBuild } from '../src/environment-build/environment-build-do.js'; export { PreviewPortTestDO } from './integration/helpers/preview-port-test-do.js'; type TestEnv = { diff --git a/services/cloud-agent-next/worker-configuration.d.ts b/services/cloud-agent-next/worker-configuration.d.ts index 9f5f5cb2633..65fdeb057dd 100644 --- a/services/cloud-agent-next/worker-configuration.d.ts +++ b/services/cloud-agent-next/worker-configuration.d.ts @@ -1,9 +1,8 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 5b7e20bbc64d17d3119ab9c0b51e431a) +// Generated by Wrangler by running `wrangler types` (hash: f594ba472384b36e719ef4f4845aba24) // Runtime types generated with workerd@1.20260918.1 2026-06-03 nodejs_compat interface __BaseEnv_Env { SHARED_SANDBOX_OVERRIDES: KVNamespace; - REPO_SNAPSHOTS: KVNamespace; R2_BUCKET: R2Bucket; RUNTIME_RELEASE_BUCKET?: R2Bucket; HYPERDRIVE: Hyperdrive; @@ -11,38 +10,59 @@ 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: "*"; + ENVIRONMENT_SNAPSHOT_ORG_IDS: "" | "9d278969-5453-4ae3-a51f-a8d2274a7b56"; 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: ""; + 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; + PREVIEW_HOST_SUFFIX: string; + VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: string; + VERCEL_SANDBOX_EXTEND_DURATION_MS: string; + LOG_REJECTED_KILO_URLS: string; Sandbox: DurableObjectNamespace; SandboxSmall: DurableObjectNamespace; SandboxDIND: DurableObjectNamespace; @@ -55,6 +75,7 @@ interface __BaseEnv_Env { SANDBOX_CONTROL: DurableObjectNamespace; SANDBOX_SESSION: DurableObjectNamespace; SANDBOX_CONTAINERS: DurableObjectNamespace; + ENVIRONMENT_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,17 +83,14 @@ 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"; } 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" | "EnvironmentBuild"; } interface DevEnv { SHARED_SANDBOX_OVERRIDES: KVNamespace; - REPO_SNAPSHOTS: KVNamespace; R2_BUCKET: R2Bucket; RUNTIME_RELEASE_BUCKET: R2Bucket; HYPERDRIVE: Hyperdrive; @@ -80,20 +98,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"; @@ -103,15 +111,46 @@ declare namespace Cloudflare { RUNTIME_ISOLATION_ENABLED: "true"; SANDBOX_SELECTION_IDS: "*"; CLOUDFLARE_CONTAINERS_ORG_IDS: "*"; + ENVIRONMENT_SNAPSHOT_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: ""; + 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; + PREVIEW_HOST_SUFFIX: string; + VERCEL_SANDBOX_INITIAL_TIMEOUT_MS: string; + VERCEL_SANDBOX_EXTEND_DURATION_MS: string; + LOG_REJECTED_KILO_URLS: string; Sandbox: DurableObjectNamespace; SandboxSmall: DurableObjectNamespace; SandboxDIND: DurableObjectNamespace; @@ -124,6 +163,7 @@ declare namespace Cloudflare { SANDBOX_CONTROL: DurableObjectNamespace; SANDBOX_SESSION: DurableObjectNamespace; SANDBOX_CONTAINERS: DurableObjectNamespace; + ENVIRONMENT_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,7 +176,7 @@ 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; diff --git a/services/cloud-agent-next/wrangler.jsonc b/services/cloud-agent-next/wrangler.jsonc index 230b7c1a2f8..5174a8d4f4b 100644 --- a/services/cloud-agent-next/wrangler.jsonc +++ b/services/cloud-agent-next/wrangler.jsonc @@ -74,8 +74,8 @@ "CONTAINER_BILLING_HEARTBEAT_SECONDS": "300", "RUNTIME_ISOLATION_ENABLED": "true", "SANDBOX_SELECTION_IDS": "9d278969-5453-4ae3-a51f-a8d2274a7b56,30f1620a-4aad-4456-bf4d-550f335e6f55", - "CONTAINER_REPO_SNAPSHOT_IDS": "", "CLOUDFLARE_CONTAINERS_ORG_IDS": "*", + "ENVIRONMENT_SNAPSHOT_ORG_IDS": "9d278969-5453-4ae3-a51f-a8d2274a7b56", "PREVIEW_HOST_SUFFIX": "cloud-agent.kiloapps.ai", "PREVIEW_ENABLED_IDS": "*", "VERCEL_SANDBOX_ORG_IDS": "", @@ -345,6 +345,10 @@ "class_name": "SandboxContainers", "name": "SANDBOX_CONTAINERS", }, + { + "class_name": "EnvironmentBuild", + "name": "ENVIRONMENT_BUILD", + }, ], }, "migrations": [ @@ -396,6 +400,10 @@ "new_sqlite_classes": ["SandboxContainers"], "tag": "v11", }, + { + "new_sqlite_classes": ["EnvironmentBuild"], + "tag": "v12", + }, ], /** * Hyperdrive Bindings @@ -412,9 +420,6 @@ { "binding": "SHARED_SANDBOX_OVERRIDES", }, - { - "binding": "REPO_SNAPSHOTS", - }, ], /** @@ -470,9 +475,6 @@ { "binding": "SHARED_SANDBOX_OVERRIDES", }, - { - "binding": "REPO_SNAPSHOTS", - }, ], "vars": { "KILOCODE_BACKEND_BASE_URL": "http://localhost:3000", @@ -498,6 +500,7 @@ "RUNTIME_ISOLATION_ENABLED": "true", "SANDBOX_SELECTION_IDS": "*", "CLOUDFLARE_CONTAINERS_ORG_IDS": "*", + "ENVIRONMENT_SNAPSHOT_ORG_IDS": "", "PREVIEW_ENABLED_IDS": "*", "VERCEL_SANDBOX_ORG_IDS": "", "VERCEL_PROJECT_ID": "", @@ -691,6 +694,10 @@ "class_name": "SandboxContainers", "name": "SANDBOX_CONTAINERS", }, + { + "class_name": "EnvironmentBuild", + "name": "ENVIRONMENT_BUILD", + }, ], }, "queues": { diff --git a/services/cloud-agent-next/wrangler.test.jsonc b/services/cloud-agent-next/wrangler.test.jsonc index 36680cdd55b..cdb4071cb75 100644 --- a/services/cloud-agent-next/wrangler.test.jsonc +++ b/services/cloud-agent-next/wrangler.test.jsonc @@ -33,6 +33,10 @@ "class_name": "PreviewPortTestDO", "name": "PREVIEW_PORT_TEST", }, + { + "class_name": "EnvironmentBuild", + "name": "ENVIRONMENT_BUILD", + }, ], }, "migrations": [ @@ -56,6 +60,10 @@ "new_classes": ["PreviewPortTestDO"], "tag": "v11", }, + { + "new_sqlite_classes": ["EnvironmentBuild"], + "tag": "v12", + }, ], "kv_namespaces": [{ "binding": "SHARED_SANDBOX_OVERRIDES" }], "r2_buckets": [ diff --git a/services/cloud-agent-next/wrapper/src/control-plane/directory-key-flip.test.ts b/services/cloud-agent-next/wrapper/src/control-plane/directory-key-flip.test.ts index d39d4bdefc4..ab891981e03 100644 --- a/services/cloud-agent-next/wrapper/src/control-plane/directory-key-flip.test.ts +++ b/services/cloud-agent-next/wrapper/src/control-plane/directory-key-flip.test.ts @@ -211,7 +211,7 @@ function createHarness() { sessionExists: async () => true, seedRegistration: async () => undefined, runSetup: async () => ({ stdout: '', stderr: '', exitCode: 0 }), - homeRoot: HOME_ROOT, + homeRoots: { directory: HOME_ROOT, legacy: HOME_ROOT }, }); preparationRef.current = preparation; @@ -219,7 +219,6 @@ function createHarness() { preparation: preparationRef, turns: turnsRef, terminals: terminalsRef, - capture: { current: undefined }, worktreeChanges: { current: undefined }, worktreeDeletion: { current: undefined }, kiloRequests: { current: undefined }, @@ -270,12 +269,9 @@ describe('directory-key flip (production dispatcher)', () => { const runtime = h.runtimes.get(DIRECTORY); expect(runtime).toBeDefined(); - // Siblings share one HOME keyed by the worktree scope and directory. + // Siblings share one HOME keyed by the directory alone. expect(runtime!.env.HOME).toBe( - path.join( - HOME_ROOT, - createHash('sha256').update(SCOPE_ID).update('\0').update(DIRECTORY).digest('hex') - ) + path.join(HOME_ROOT, createHash('sha256').update(DIRECTORY).digest('hex')) ); const client = runtime!.client as unknown as { sendPromptAsync: (opts: { sessionId: string }) => Promise; @@ -421,7 +417,7 @@ describe('directory-key flip (production dispatcher)', () => { } }); - it('keys shared HOME by worktree scope and directory', async () => { + it('keys shared HOME by directory', async () => { const h = createHarness(); try { const first = routeSpec('ses_a', 'ses_kilo_a', { @@ -445,15 +441,12 @@ describe('directory-key flip (production dispatcher)', () => { // The two routes are distinct runtimes (distinct directories), so read each // HOME from the runtime the preparation built. - const homeFor = (key: string, directory: string): string => - path.join( - HOME_ROOT, - createHash('sha256').update(key).update('\0').update(directory).digest('hex') - ); + const homeFor = (directory: string): string => + path.join(HOME_ROOT, createHash('sha256').update(directory).digest('hex')); const firstRuntime = h.runtimes.get(`${DIRECTORY}/one`); const secondRuntime = h.runtimes.get(`${DIRECTORY}/two`); - expect(firstRuntime?.env.HOME).toBe(homeFor(SCOPE_ID, `${DIRECTORY}/one`)); - expect(secondRuntime?.env.HOME).toBe(homeFor(SCOPE_ID, `${DIRECTORY}/two`)); + expect(firstRuntime?.env.HOME).toBe(homeFor(`${DIRECTORY}/one`)); + expect(secondRuntime?.env.HOME).toBe(homeFor(`${DIRECTORY}/two`)); // Same scope, different directory: HOME differs (directory is hashed in). expect(firstRuntime?.env.HOME).not.toBe(secondRuntime?.env.HOME); } finally { diff --git a/services/cloud-agent-next/wrapper/src/control-plane/main.ts b/services/cloud-agent-next/wrapper/src/control-plane/main.ts index 2f43903ac15..32a1e517980 100644 --- a/services/cloud-agent-next/wrapper/src/control-plane/main.ts +++ b/services/cloud-agent-next/wrapper/src/control-plane/main.ts @@ -31,7 +31,6 @@ import { liveRuntimeClient, } from './kilo-runtime.js'; import { createPreparationManager, runtimeKey, type PreparationManager } from './prepare.js'; -import { createWorkspaceCapture, type WorkspaceCapture } from './workspace-capture.js'; import { createTurnManager, type TurnManager, type TurnManagerDeps } from './turn.js'; import { createControlPlaneTerminals, type ControlPlaneTerminalRuntime } from './terminals.js'; import { createNativeStatusReporter } from './native-status.js'; @@ -214,7 +213,6 @@ export type ControlPlaneFrameDispatcherDeps = { preparation: { current?: PreparationManager }; turns: { current?: TurnManager }; terminals: { current?: ControlPlaneTerminalRuntime }; - capture: { current?: WorkspaceCapture }; worktreeChanges: { current?: ReturnType }; worktreeDeletion: { current?: ReturnType }; kiloRequests: { current?: ReturnType }; @@ -252,7 +250,7 @@ export function createControlPlaneFrameDispatcher( .catch(() => undefined); return; case 'workspace.captured': - deps.capture.current?.onCaptured(frame.sessionId, frame.ok); + // Sessions never capture; a reply to a pre-change request is ignored. return; case 'session.credentials': void deps.preparation.current?.installCredentials(frame).catch(() => { @@ -355,9 +353,6 @@ export async function runControlPlaneWrapper( // Preparation is created after the connection (it emits through it), so the // connection reaches it through a holder. const preparationRef: { current?: ReturnType } = {}; - // A repository capture is requested by preparation and answered by a frame the - // connection delivers, so the connection reaches it through a holder. - const captureRef: { current?: WorkspaceCapture } = {}; // Turns share work with preparation but need the runtimes' restart callbacks, // so both sides reach each other through holders. const turnsRef: { current?: TurnManager } = {}; @@ -408,7 +403,6 @@ export async function runControlPlaneWrapper( preparation: preparationRef, turns: turnsRef, terminals: terminalsRef, - capture: captureRef, worktreeChanges: worktreeChangesRef, worktreeDeletion: worktreeDeletionRef, kiloRequests: kiloRequestsRef, @@ -424,12 +418,10 @@ export async function runControlPlaneWrapper( wrapperId: connection.wrapperId, runtimes, }); - captureRef.current = createWorkspaceCapture({ send: frame => connection.send(frame) }); preparationRef.current = createPreparationManager({ timers, onNativeDiagnostic: projector, allocationId, - capture: captureRef.current, runtimes: { ensure: input => runtimes.ensure(input), installCredentials: async (key, nextEnv, cloudAgentMcp) => { diff --git a/services/cloud-agent-next/wrapper/src/control-plane/prepare-adopt-git.test.ts b/services/cloud-agent-next/wrapper/src/control-plane/prepare-adopt-git.test.ts index db93cf0a677..aa41165273b 100644 --- a/services/cloud-agent-next/wrapper/src/control-plane/prepare-adopt-git.test.ts +++ b/services/cloud-agent-next/wrapper/src/control-plane/prepare-adopt-git.test.ts @@ -11,13 +11,7 @@ import type { } from '../../../src/shared/control-plane-protocol.js'; import type { WrapperKiloClient } from '../kilo-api.js'; import { createPreparationManager } from './prepare.js'; -import { - readWorkspaceStamp, - SNAPSHOT_MAX_GENERATION, - SNAPSHOT_REFRESH_AFTER_MS, -} from './workspace-stamp.js'; - -const T0 = 1_800_000_000_000; +import { readWorkspaceStamp } from './workspace-stamp.js'; // These tests run real git against a local origin. A mocked git proves the command // sequence; only a real repository proves an adopted snapshot ends on the same @@ -86,34 +80,12 @@ function spec(overrides: Partial = {}): ControlPlaneRoute }; } -type GitState = { origin: string; fetchHead: boolean; reflog: string }; - -/** What `.git` holds at the moment the Sandbox DO would snapshot the container. */ -async function gitStateAtCapture(): Promise { - return { - origin: git(directory, 'remote', 'get-url', 'origin'), - fetchHead: await fs - .access(path.join(directory, '.git', 'FETCH_HEAD')) - .then(() => true) - .catch(() => false), - reflog: git(directory, 'reflog', '--all'), - }; -} - -function manager(allocationId: string, now = T0) { +function manager(allocationId: string) { const frames: ControlPlaneWrapperFrame[] = []; const gitCalls: string[][] = []; - const captures: GitState[] = []; const prepared = createPreparationManager({ timers: CONTROL_PLANE_TIMERS, allocationId, - now: () => now, - capture: { - request: async () => { - captures.push(await gitStateAtCapture()); - return true; - }, - }, emit: frame => frames.push(frame), runtimes: { ensure: async () => ({ serverUrl: 'http://127.0.0.1:1' }) as unknown as WrapperKiloClient, @@ -124,7 +96,7 @@ function manager(allocationId: string, now = T0) { release: () => undefined, }, inheritedEnv: { PATH: process.env.PATH ?? '', ...GIT_ENV } as NodeJS.ProcessEnv, - homeRoot: path.join(root, 'homes'), + homeRoots: { directory: path.join(root, 'homes'), legacy: path.join(root, 'legacy-homes') }, runGit: async (args, options) => { gitCalls.push(args); const { git: run } = await import('../utils.js'); @@ -134,19 +106,20 @@ function manager(allocationId: string, now = T0) { sessionExists: async () => true, sleep: async () => undefined, }); - return { prepared, frames, gitCalls, captures }; + return { prepared, frames, gitCalls }; } function ready(frames: ControlPlaneWrapperFrame[]) { return frames.findLast(frame => frame.type === 'session.ready'); } -describe('adopting a repository snapshot against a real repository', () => { +// The first manager stands in for the environment builder: another allocation +// prepared the workspace, so the second classifies it as a snapshot to adopt. +describe('adopting an environment snapshot against a real repository', () => { it('starts a new session branch from the current origin tip, not the snapshot commit', async () => { const first = manager('alloc-image'); - await first.prepared.prepare(spec({ capture: true })); + await first.prepared.prepare(spec()); expect(ready(first.frames)).toMatchObject({ workspace: 'cloned' }); - expect(first.captures).toHaveLength(1); const snapshotCommit = git(directory, 'rev-parse', 'HEAD'); const tip = commitFile('README', 'two', 'second'); @@ -166,10 +139,7 @@ describe('adopting a repository snapshot against a real repository', () => { expect(await readWorkspaceStamp(directory, true)).toEqual({ allocationId: 'alloc-restored', commit: tip, - capturedAt: T0, - generation: 0, }); - expect(restored.captures).toEqual([]); }); it("gives an existing session the branch it pushed, not the snapshot's stale one", async () => { @@ -252,64 +222,3 @@ describe('adopting a repository snapshot against a real repository', () => { expect(second.gitCalls).toEqual([]); }); }); - -describe('refreshing an adopted snapshot against a real repository', () => { - it('captures an old snapshot from the adopted workspace, with no credential left in .git', async () => { - const first = manager('alloc-image', T0); - await first.prepared.prepare(spec({ capture: true })); - await fs.mkdir(path.join(directory, 'node_modules'), { recursive: true }); - await fs.writeFile(path.join(directory, 'node_modules', 'dep.js'), 'module.exports = 1'); - const tip = commitFile('README', 'two', 'second'); - git(author, 'push', '-q', 'origin', 'main'); - - const refreshed = manager('alloc-refresh', T0 + SNAPSHOT_REFRESH_AFTER_MS); - await refreshed.prepared.prepare(spec({ sessionId: 'ses_b', capture: true })); - - expect(ready(refreshed.frames)).toMatchObject({ workspace: 'adopted' }); - expect(refreshed.gitCalls.some(args => args[0] === 'clone')).toBe(false); - expect(refreshed.captures).toHaveLength(1); - const [atCapture] = refreshed.captures; - expect(atCapture).toEqual({ - origin: pathToFileURL(origin).toString(), - fetchHead: false, - reflog: '', - }); - expect(await readWorkspaceStamp(directory, true)).toEqual({ - allocationId: 'alloc-refresh', - commit: tip, - capturedAt: T0 + SNAPSHOT_REFRESH_AFTER_MS, - generation: 1, - }); - expect(await fs.readFile(path.join(directory, 'node_modules', 'dep.js'), 'utf8')).toBe( - 'module.exports = 1' - ); - }); - - it('does not capture before the snapshot is due, and rebuilds from a clone at the generation cap', async () => { - await manager('alloc-image', T0).prepared.prepare(spec({ capture: true })); - - const early = manager('alloc-early', T0 + SNAPSHOT_REFRESH_AFTER_MS - 1); - await early.prepared.prepare(spec({ sessionId: 'ses_b', capture: true })); - expect(ready(early.frames)).toMatchObject({ workspace: 'adopted' }); - expect(early.captures).toEqual([]); - - let at = T0; - for (let generation = 1; generation <= SNAPSHOT_MAX_GENERATION; generation += 1) { - at += SNAPSHOT_REFRESH_AFTER_MS; - const next = manager(`alloc-refresh-${generation}`, at); - await next.prepared.prepare(spec({ sessionId: `ses_${generation}`, capture: true })); - expect(next.captures).toHaveLength(1); - expect(await readWorkspaceStamp(directory, true)).toMatchObject({ generation }); - } - - const rebuilt = manager('alloc-rebuilt', at + SNAPSHOT_REFRESH_AFTER_MS); - await rebuilt.prepared.prepare(spec({ sessionId: 'ses_rebuilt', capture: true })); - expect(ready(rebuilt.frames)).toMatchObject({ workspace: 'cloned' }); - expect(rebuilt.gitCalls.filter(args => args[0] === 'clone')).toHaveLength(1); - expect(rebuilt.captures).toHaveLength(1); - expect(await readWorkspaceStamp(directory, true)).toMatchObject({ - capturedAt: at + SNAPSHOT_REFRESH_AFTER_MS, - generation: 0, - }); - }); -}); diff --git a/services/cloud-agent-next/wrapper/src/control-plane/prepare.test.ts b/services/cloud-agent-next/wrapper/src/control-plane/prepare.test.ts index 566d5a9d13f..b2384ef5222 100644 --- a/services/cloud-agent-next/wrapper/src/control-plane/prepare.test.ts +++ b/services/cloud-agent-next/wrapper/src/control-plane/prepare.test.ts @@ -20,11 +20,8 @@ import type { WrapperKiloClient } from '../kilo-api.js'; import type { ExecResult, ProcessOptions, ProcessOutputStream } from '../utils.js'; import * as processUtils from '../utils.js'; import { createPreparationManager, type PrepareRuntimePort } from './prepare.js'; -import { - SNAPSHOT_MAX_GENERATION, - SNAPSHOT_REFRESH_AFTER_MS, - type WorkspaceStamp, -} from './workspace-stamp.js'; +import type { WorkspaceStamp } from './workspace-stamp.js'; +import { directoryHome } from './workspace-home.js'; const NOW = 1_800_000_000_000; import { @@ -36,7 +33,6 @@ import { } from './kilo-runtime.js'; import type { restoreWorktreeRecovery } from './worktree-recovery.js'; import { restoreSession } from '../restore-session.js'; -import { createWorkspaceCapture } from './workspace-capture.js'; describe('replacement workspace local recovery', () => { const recovery = { @@ -121,13 +117,7 @@ describe('replacement workspace local recovery', () => { }, ], }; - const harness = createHarness(FAST_TIMERS, { - capture: true, - beforeCapture: async () => { - expect(await fs.readFile(path.join(directory, 'lost.txt'), 'utf8')).toBe('newer local\n'); - expect(harness.ensureCalls()).toBe(0); - }, - }); + const harness = createHarness(FAST_TIMERS); harness.setSessionExists(false); harness.setRestore(async () => restoreSession(routeSpec().kiloSessionId, directory, undefined, { @@ -144,15 +134,134 @@ describe('replacement workspace local recovery', () => { routeSpec({ directory, git: { url: 'https://github.com/acme/repo.git' }, - capture: true, worktreeRecovery: newer, }) ); expect(await fs.readFile(path.join(directory, 'lost.txt'), 'utf8')).toBe('newer local\n'); - expect(harness.captureRequests).toHaveLength(1); - expect(JSON.parse(await fs.readFile(imported, 'utf8')).sessionDiff).toEqual( - snapshot.sessionDiff + expect(harness.frames.some(frame => frame.type === 'session.ready')).toBe(true); + } finally { + fetchSpy.mockRestore(); + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it('keeps recovered tracked and added bytes on a stamped same-allocation restart with older ingest', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'prepare-restart-recovery-')); + const directory = path.join(root, 'workspace'); + const imported = path.join(root, 'imported.json'); + const bin = path.join(root, 'bin'); + const snapshot = { + info: { id: routeSpec().kiloSessionId, version: '2' }, + messages: [], + sessionDiff: [ + { file: 'tracked.txt', status: 'modified', after: 'older tracked\n' }, + { file: 'added.txt', status: 'added', after: 'older added\n' }, + ], + }; + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue( + new Response(JSON.stringify(snapshot), { status: 200 }) + ); + try { + await fs.mkdir(directory); + await fs.mkdir(bin); + expect(Bun.spawnSync(['git', 'init', directory]).exitCode).toBe(0); + await fs.writeFile(path.join(directory, 'tracked.txt'), 'recovered tracked\n'); + await fs.writeFile(path.join(directory, 'added.txt'), 'recovered added\n'); + await fs.writeFile(path.join(bin, 'kilo'), `#!/bin/sh\ncp "$2" "${imported}"\n`, { + mode: 0o755, + }); + const harness = createHarness(FAST_TIMERS, { hasGit: true }); + harness.setStamp({ + allocationId: 'alloc-current', + commit: 'c', + capturedAt: NOW, + generation: 0, + }); + harness.setSessionExists(false); + harness.setRestore(async () => + restoreSession(routeSpec().kiloSessionId, directory, undefined, { + restoreFiles: harness.restoreOptions()[0]?.restoreFiles !== false, + env: { + ...process.env, + PATH: `${bin}:${process.env.PATH}`, + KILO_RUNTIME_CLI_PATH: path.join(bin, 'kilo'), + KILO_SESSION_INGEST_URL: 'https://ingest.test', + KILOCODE_TOKEN: 'test-token', + }, + }) + ); + await harness.manager.prepare( + routeSpec({ + directory, + git: { url: 'https://github.com/acme/repo.git' }, + worktreeRecovery: { + files: [ + { path: 'tracked.txt', status: 'modified', patch: 'saved' }, + { path: 'added.txt', status: 'added', patch: 'saved' }, + ], + }, + }) + ); + expect(harness.restoreOptions()[0]?.restoreFiles).toBe(false); + expect(await fs.readFile(path.join(directory, 'tracked.txt'), 'utf8')).toBe( + 'recovered tracked\n' + ); + expect(await fs.readFile(path.join(directory, 'added.txt'), 'utf8')).toBe( + 'recovered added\n' + ); + expect(harness.frames.some(frame => frame.type === 'session.ready')).toBe(true); + } finally { + fetchSpy.mockRestore(); + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it('imports ingest files for a prepare with no recovery', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'prepare-no-recovery-')); + const directory = path.join(root, 'workspace'); + const bin = path.join(root, 'bin'); + const snapshot = { + info: { id: routeSpec().kiloSessionId, version: '2' }, + messages: [], + sessionDiff: [{ file: 'lost.txt', status: 'modified', after: 'older ingest\n' }], + }; + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue( + new Response(JSON.stringify(snapshot), { status: 200 }) + ); + try { + await fs.mkdir(directory); + await fs.mkdir(bin); + expect(Bun.spawnSync(['git', 'init', directory]).exitCode).toBe(0); + await fs.writeFile(path.join(directory, 'lost.txt'), 'base\n'); + await fs.writeFile( + path.join(bin, 'kilo'), + `#!/bin/sh\ncp "$2" "${path.join(root, 'imported.json')}"\n`, + { + mode: 0o755, + } + ); + const harness = createHarness(FAST_TIMERS); + harness.setSessionExists(false); + harness.setRestore(async () => + restoreSession(routeSpec().kiloSessionId, directory, undefined, { + restoreFiles: harness.restoreOptions()[0]?.restoreFiles !== false, + env: { + ...process.env, + PATH: `${bin}:${process.env.PATH}`, + KILO_RUNTIME_CLI_PATH: path.join(bin, 'kilo'), + KILO_SESSION_INGEST_URL: 'https://ingest.test', + KILOCODE_TOKEN: 'test-token', + }, + }) + ); + await harness.manager.prepare( + routeSpec({ + directory, + git: { url: 'https://github.com/acme/repo.git' }, + }) ); + expect(harness.restoreOptions()[0]?.restoreFiles).toBe(true); + expect(await fs.readFile(path.join(directory, 'lost.txt'), 'utf8')).toBe('older ingest\n'); expect(harness.frames.some(frame => frame.type === 'session.ready')).toBe(true); } finally { fetchSpy.mockRestore(); @@ -204,6 +313,257 @@ describe('replacement workspace local recovery', () => { expect(harness.frames.some(frame => frame.type === 'session.ready')).toBe(true); expect(harness.frames.some(frame => frame.type === 'session.failed')).toBe(false); }); + + it('keeps two repeated unstamped crashes unstamped before the fallback import', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'prepare-deferred-crash-')); + const directory = path.join(root, 'workspace'); + const bin = path.join(root, 'bin'); + const imported = path.join(root, 'imported.json'); + const snapshot = { + info: { id: routeSpec().kiloSessionId, version: '2' }, + messages: [], + sessionDiff: [ + { file: 'tracked.txt', status: 'modified', after: 'older tracked\n' }, + { file: 'added.txt', status: 'added', after: 'older added\n' }, + ], + }; + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue( + new Response(JSON.stringify(snapshot), { status: 200 }) + ); + try { + await fs.mkdir(directory); + await fs.mkdir(bin); + expect(Bun.spawnSync(['git', 'init', directory]).exitCode).toBe(0); + await fs.writeFile(path.join(directory, 'tracked.txt'), 'local base\n'); + await fs.writeFile(path.join(bin, 'kilo'), `#!/bin/sh\ncp "$2" "${imported}"\n`, { + mode: 0o755, + }); + const harness = createHarness(FAST_TIMERS, { + recoverWorktree: async () => { + throw new Error('conflicting patch'); + }, + }); + harness.setSessionExists(false); + // First and second prepares crash before the deferred import returns. + harness.setRestore(async () => { + throw new Error('import failed'); + }); + const crashed = routeSpec({ + directory, + git: { url: 'https://github.com/acme/repo.git' }, + worktreeRecovery: recovery, + }); + await harness.manager.prepare(crashed); + expect(harness.stamp()).toBeNull(); + await harness.manager.prepare(crashed); + expect(harness.stamp()).toBeNull(); + // The third prepare imports the older tracked and added bytes, then stamps. + harness.setRestore(async () => + restoreSession(routeSpec().kiloSessionId, directory, undefined, { + restoreFiles: harness.restoreOptions().at(-1)?.restoreFiles !== false, + env: { + ...process.env, + PATH: `${bin}:${process.env.PATH}`, + KILO_RUNTIME_CLI_PATH: path.join(bin, 'kilo'), + KILO_SESSION_INGEST_URL: 'https://ingest.test', + KILOCODE_TOKEN: 'test-token', + }, + }) + ); + await harness.manager.prepare(crashed); + expect(harness.restoreOptions().at(-1)?.restoreFiles).toBe(true); + expect(await fs.readFile(path.join(directory, 'tracked.txt'), 'utf8')).toBe( + 'older tracked\n' + ); + expect(await fs.readFile(path.join(directory, 'added.txt'), 'utf8')).toBe('older added\n'); + expect(harness.stamp()).not.toBeNull(); + expect(harness.frames.some(frame => frame.type === 'session.ready')).toBe(true); + } finally { + fetchSpy.mockRestore(); + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it.each(['throw', 'zero-applied'])( + 'leaves no current-allocation stamp before the deferred import for %s', + async kind => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'prepare-deferred-gate-')); + const directory = path.join(root, 'workspace'); + const bin = path.join(root, 'bin'); + const snapshot = { + info: { id: routeSpec().kiloSessionId, version: '2' }, + messages: [], + sessionDiff: [ + { file: 'tracked.txt', status: 'modified', after: 'older tracked\n' }, + { file: 'added.txt', status: 'added', after: 'older added\n' }, + ], + }; + const fetchSpy = spyOn(globalThis, 'fetch').mockResolvedValue( + new Response(JSON.stringify(snapshot), { status: 200 }) + ); + let releaseImport: () => void = () => undefined; + const importGate = new Promise(resolve => { + releaseImport = resolve; + }); + try { + await fs.mkdir(directory); + await fs.mkdir(bin); + expect(Bun.spawnSync(['git', 'init', directory]).exitCode).toBe(0); + await fs.writeFile(path.join(directory, 'tracked.txt'), 'local base\n'); + await fs.writeFile(path.join(bin, 'kilo'), `#!/bin/sh\ncp "$2" "${root}/imported.json"\n`, { + mode: 0o755, + }); + const harness = createHarness(FAST_TIMERS, { + recoverWorktree: + kind === 'throw' + ? async () => { + throw new Error('conflicting patch'); + } + : async () => ({ + applied: 0, + total: 1, + skipped: 1, + skippedDiffs: [{ file: 'lost.txt', reason: 'patch_apply_failed' as const }], + }), + beforeEnsure: async () => { + await importGate; + }, + }); + harness.setSessionExists(false); + harness.setRestore(async () => + restoreSession(routeSpec().kiloSessionId, directory, undefined, { + restoreFiles: harness.restoreOptions().at(-1)?.restoreFiles !== false, + env: { + ...process.env, + PATH: `${bin}:${process.env.PATH}`, + KILO_RUNTIME_CLI_PATH: path.join(bin, 'kilo'), + KILO_SESSION_INGEST_URL: 'https://ingest.test', + KILOCODE_TOKEN: 'test-token', + }, + }) + ); + const run = harness.manager.prepare( + routeSpec({ + directory, + git: { url: 'https://github.com/acme/repo.git' }, + worktreeRecovery: recovery, + }) + ); + for (let attempt = 0; attempt < 200 && harness.ensureCalls() === 0; attempt += 1) { + await new Promise(resolve => setTimeout(resolve, 5)); + } + expect(harness.ensureCalls()).toBeGreaterThan(0); + // Held inside the deferred import: the current-allocation stamp is absent. + expect(harness.stamp()).toBeNull(); + releaseImport(); + await run; + expect(await fs.readFile(path.join(directory, 'tracked.txt'), 'utf8')).toBe( + 'older tracked\n' + ); + expect(await fs.readFile(path.join(directory, 'added.txt'), 'utf8')).toBe('older added\n'); + expect(harness.stamp()).not.toBeNull(); + } finally { + fetchSpy.mockRestore(); + await fs.rm(root, { recursive: true, force: true }); + } + } + ); + + it('keeps a sibling from adopting or emptying the directory during a held deferred import', async () => { + let releaseImport: () => void = () => undefined; + const importGate = new Promise(resolve => { + releaseImport = resolve; + }); + let importStarted = false; + const harness = createHarness(FAST_TIMERS, { + recoverWorktree: async () => ({ + applied: 0, + total: 1, + skipped: 1, + skippedDiffs: [{ file: 'lost.txt', reason: 'patch_apply_failed' as const }], + }), + }); + harness.setSessionExists(false); + harness.setRestore(async () => { + importStarted = true; + await importGate; + return { + ok: true, + downloaded: true, + imported: true, + diffs: { applied: 1, skipped: 0, total: 1 }, + }; + }); + const directory = '/tmp/prepare-sibling'; + const spec = (sessionId: string) => + routeSpec({ + sessionId, + directory, + git: { url: 'https://github.com/acme/repo.git' }, + worktreeRecovery: recovery, + }); + const first = harness.manager.prepare(spec('ses_first_sibling')); + for (let attempt = 0; attempt < 200 && !importStarted; attempt += 1) { + await new Promise(resolve => setTimeout(resolve, 5)); + } + expect(importStarted).toBe(true); + const ensureDuringHold = harness.ensureCalls(); + const restoreDuringHold = harness.restoreCalls(); + const second = harness.manager.prepare(spec('ses_second_sibling')); + await new Promise(resolve => setTimeout(resolve, 20)); + // The sibling is queued behind the held lock: no adoption, no emptying, and + // no second import started. + expect(harness.emptyCalls).toHaveLength(0); + expect(harness.ensureCalls()).toBe(ensureDuringHold); + expect(harness.restoreCalls()).toBe(restoreDuringHold); + releaseImport(); + await Promise.all([first, second]); + // After the first prepare stamps, the sibling sees the same allocation. + expect(harness.logs.some(line => line.includes('inspection=same'))).toBe(true); + }); + + it('releases a deferred per-session runtime when released during the deferred import', async () => { + let releasePreparation: () => void = () => undefined; + const gate = new Promise(resolve => { + releasePreparation = resolve; + }); + const harness = createHarness(FAST_TIMERS, { + recoverWorktree: async () => { + throw new Error('conflicting patch'); + }, + beforeEnsure: async () => { + await gate; + }, + }); + harness.setSessionExists(false); + harness.setRestore(async () => ({ + ok: true, + downloaded: true, + imported: true, + diffs: { applied: 1, skipped: 0, total: 1 }, + })); + const sessionId = 'ses_00000000000000000000000000'; + const run = harness.manager.prepare( + routeSpec({ + sessionId, + directory: '/tmp/prepare-deferred-release', + git: { url: 'https://github.com/acme/repo.git' }, + runtimeIsolation: 'per-session', + worktreeRecovery: recovery, + }) + ); + for (let attempt = 0; attempt < 200 && harness.ensureCalls() === 0; attempt += 1) { + await new Promise(resolve => setTimeout(resolve, 5)); + } + expect(harness.ensureCalls()).toBeGreaterThan(0); + // Stop/supersede while the deferred import holds the lock. + harness.manager.release(sessionId); + releasePreparation(); + await run; + expect(harness.stamp()).toBeNull(); + expect(harness.releaseCalls).toHaveLength(1); + expect(harness.frames.some(frame => frame.type === 'session.ready')).toBe(false); + }); }); function timers(overrides: Partial = {}): ControlPlaneTimers { @@ -285,31 +645,25 @@ type Harness = { stamp: () => WorkspaceStamp | null; setHasGit: (value: boolean) => void; emptyCalls: string[]; - clearedHomes: string[]; - truncations: () => number; - captureRequests: Array<{ sessionId: string; commit: string | undefined; timeoutMs: number }>; - setCaptureResult: (value: boolean | 'throw') => void; setSetupResult: (value: ExecResult) => void; setSetupOutput: ( value: (onOutput: (stream: ProcessOutputStream, output: string) => void) => void ) => void; }; +/** Both home roots under one test directory, as the wrapper keeps them apart in production. */ +function homeRootsFor(root: string) { + return { directory: path.join(root, 'home'), legacy: path.join(root, 'kilo-worktrees') }; +} + function createHarness( activeTimers: ControlPlaneTimers = FAST_TIMERS, options: { hasGit?: boolean; - capture?: boolean; beforeEnsure?: () => Promise; homeRoot?: string; beforeInstall?: () => Promise; recoverWorktree?: typeof restoreWorktreeRecovery; - beforeCapture?: () => Promise; - captureRequest?: ( - sessionId: string, - commit: string | undefined, - timeoutMs: number - ) => Promise; beforeWorkspace?: () => Promise; releaseCleanupDeadlineMs?: number; runtimes?: PrepareRuntimePort; @@ -357,10 +711,6 @@ function createHarness( let stamp: WorkspaceStamp | null = null; let hasGitState = options.hasGit ?? false; const emptyCalls: string[] = []; - const clearedHomes: string[] = []; - let truncations = 0; - const captureRequests: Harness['captureRequests'] = []; - let captureResult: boolean | 'throw' = true; const unavailableKeys = new Set(); let activeClones = 0; let maxActiveClones = 0; @@ -452,26 +802,11 @@ function createHarness( onNativeDiagnostic: (event, fields) => nativeDiagnostics.push({ event, fields }), runtimes, inheritedEnv: {}, - homeRoot: options.homeRoot ?? '/tmp/prepare-test-homes', + homeRoots: homeRootsFor(options.homeRoot ?? '/tmp/prepare-test-homes'), allocationId: 'alloc-current', - now: () => NOW, ...(options.releaseCleanupDeadlineMs === undefined ? {} : { releaseCleanupDeadlineMs: options.releaseCleanupDeadlineMs }), - ...(options.capture - ? { - capture: { - request: async (sessionId: string, commit: string | undefined, timeoutMs: number) => { - await options.beforeCapture?.(); - captureRequests.push({ sessionId, commit, timeoutMs }); - if (options.captureRequest) - return options.captureRequest(sessionId, commit, timeoutMs); - if (captureResult === 'throw') throw new Error('capture channel failed'); - return captureResult; - }, - }, - } - : {}), hasGit: async () => hasGitState, readStamp: async () => stamp, writeStamp: async (_directory, value) => { @@ -482,12 +817,6 @@ function createHarness( hasGitState = false; stamp = null; }, - clearStaleHomes: async (_root, keep) => { - clearedHomes.push(keep); - }, - truncateLog: async () => { - truncations += 1; - }, mkdir: async () => undefined, configureGitAuthor: async (_directory, _runGit, author) => { authorCalls.push(author); @@ -596,12 +925,6 @@ function createHarness( hasGitState = value; }, emptyCalls, - clearedHomes, - truncations: () => truncations, - captureRequests, - setCaptureResult: value => { - captureResult = value; - }, setSetupResult: value => { setupResult = value; }, @@ -1538,7 +1861,7 @@ describe('createPreparationManager', () => { release: () => undefined, }, inheritedEnv: {}, - homeRoot: '/tmp/prepare-test-homes', + homeRoots: homeRootsFor('/tmp/prepare-test-homes'), hasGit: async () => false, readStamp: async () => null, writeStamp: async () => undefined, @@ -1771,7 +2094,7 @@ describe('createPreparationManager', () => { ).toBe(true); }); - it('keys shared HOME by worktree scope and directory', async () => { + it('keys a worktree runtime HOME by directory alone and keeps per-session homes', async () => { const harness = createHarness(); const directory = '/tmp/home-scope-one'; const worktree = routeSpec({ @@ -1779,22 +2102,42 @@ describe('createPreparationManager', () => { kiloSessionId: 'kilo_a', directory, }); - // A non-worktree modern session carries its own session id as the scope id. - const nonWorktree = routeSpec({ + // A sibling in the same directory with another scope shares the same HOME. + const sibling = routeSpec({ sessionId: 'ses_b', kiloSessionId: 'kilo_b', - directory: '/tmp/home-non-worktree', + directory, kilo: { ...routeSpec().kilo!, scopeId: 'ses_b' }, }); + // An existing allocation keeps its per-session runtime and HOME. + const perSession = routeSpec({ + sessionId: 'ses_c', + kiloSessionId: 'kilo_c', + directory: '/tmp/home-per-session', + runtimeIsolation: 'per-session', + }); await harness.manager.prepare(worktree); - await harness.manager.prepare(nonWorktree); - - const homeRoot = '/tmp/prepare-test-homes'; - const expected = (key: string, dir: string): string => - path.join(homeRoot, createHash('sha256').update(key).update('\0').update(dir).digest('hex')); - expect(harness.ensureInputs[0]!.env.HOME).toBe(expected('scope-1', directory)); - expect(harness.ensureInputs[1]!.env.HOME).toBe(expected('ses_b', '/tmp/home-non-worktree')); + await harness.manager.prepare(sibling); + await harness.manager.prepare(perSession); + + const roots = homeRootsFor('/tmp/prepare-test-homes'); + expect(harness.ensureInputs[0]!.env.HOME).toBe(directoryHome(directory, roots.directory)); + expect(harness.ensureInputs[1]!.env.HOME).toBe(directoryHome(directory, roots.directory)); + expect(harness.ensureInputs[2]!.env.HOME).toBe( + path.join( + roots.legacy, + createHash('sha256') + .update('ses_c') + .update('\0') + .update('/tmp/home-per-session') + .digest('hex') + ) + ); + // The builder computes the same fixed home for the isolated workspace. + expect(directoryHome('/workspace/app')).toBe( + path.join('/home', createHash('sha256').update('/workspace/app').digest('hex')) + ); }); it('re-prepares a prepared route whose runtime is unavailable', async () => { @@ -2000,7 +2343,7 @@ describe('createPreparationManager', () => { release: () => undefined, }, inheritedEnv: {}, - homeRoot: '/tmp/prepare-test-homes', + homeRoots: homeRootsFor('/tmp/prepare-test-homes'), allocationId: 'alloc-current', hasGit: async () => true, readStamp: async () => null, @@ -2177,22 +2520,16 @@ describe('createPreparationManager', () => { }); }); -const FOREIGN_STAMP = { - allocationId: 'alloc-previous', - commit: 'old-commit', - capturedAt: NOW - 60_000, - generation: 0, -}; -const DUE_STAMP = { ...FOREIGN_STAMP, capturedAt: NOW - SNAPSHOT_REFRESH_AFTER_MS }; +const FOREIGN_STAMP = { allocationId: 'alloc-builder', commit: 'old-commit' }; const REPO = 'https://github.com/acme/repo.git'; function gitKinds(calls: string[][]): string[] { return calls.map(args => args.slice(0, args[0] === 'remote' ? 2 : 1).join(' ')); } -describe('adopting a repository snapshot', () => { - function adoptHarness(options: { capture?: boolean } = {}) { - const harness = createHarness(FAST_TIMERS, { hasGit: true, ...options }); +describe('adopting an environment snapshot', () => { + function adoptHarness() { + const harness = createHarness(FAST_TIMERS, { hasGit: true }); harness.setStamp(FOREIGN_STAMP); harness.setGit(args => { if (args[0] === 'show-ref') return result(1); @@ -2237,20 +2574,24 @@ describe('adopting a repository snapshot', () => { 'kilo_runtime', 'kilo_session', ]); - expect(harness.stamp()).toEqual({ - allocationId: 'alloc-current', - commit: 'new-commit', - capturedAt: FOREIGN_STAMP.capturedAt, - generation: 0, - }); + expect(harness.stamp()).toEqual({ allocationId: 'alloc-current', commit: 'new-commit' }); expect(lastFrame(harness.frames)).toEqual({ type: 'session.ready', sessionId: spec.sessionId, workspace: 'adopted', }); expect(harness.emptyCalls).toEqual([]); - expect(harness.clearedHomes).toHaveLength(1); - expect(harness.clearedHomes[0]).toContain('/tmp/prepare-test-homes/'); + }); + + it('adopts a stamp written by a snapshot-capturing wrapper', async () => { + const harness = adoptHarness(); + harness.setStamp({ ...FOREIGN_STAMP, capturedAt: NOW - 60_000, generation: 4 }); + + await harness.manager.prepare(routeSpec({ git: { url: REPO } })); + + expect(harness.gitCalls.some(args => args[0] === 'clone')).toBe(false); + expect(lastFrame(harness.frames)).toMatchObject({ workspace: 'adopted' }); + expect(harness.stamp()).toEqual({ allocationId: 'alloc-current', commit: 'new-commit' }); }); it('checks out an explicit branch with the ordinary branch logic', async () => { @@ -2403,291 +2744,31 @@ describe('adopting a repository snapshot', () => { it('does nothing for a workspace this allocation already prepared', async () => { const harness = createHarness(FAST_TIMERS, { hasGit: true }); - const prepared = { - allocationId: 'alloc-current', - commit: 'abc', - capturedAt: NOW, - generation: 0, - }; + const prepared = { allocationId: 'alloc-current', commit: 'abc' }; harness.setStamp(prepared); const spec = routeSpec({ git: { url: REPO }, setupCommands: ['npm install'] }); await harness.manager.prepare(spec); expect(harness.gitCalls).toEqual([]); - expect(harness.clearedHomes).toEqual([]); expect(progressSteps(harness.frames)).toEqual(['kilo_runtime', 'kilo_session']); expect(lastFrame(harness.frames)).toMatchObject({ workspace: 'same' }); expect(harness.stamp()).toEqual(prepared); }); it('reuses a repository without a stamp, never captures it, and stamps it', async () => { - const harness = createHarness(FAST_TIMERS, { hasGit: true, capture: true }); - const spec = routeSpec({ git: { url: REPO }, capture: true }); + const harness = createHarness(FAST_TIMERS, { hasGit: true }); + const spec = routeSpec({ git: { url: REPO } }); await harness.manager.prepare(spec); expect(harness.gitCalls.some(args => args[0] === 'clone')).toBe(false); expect(harness.gitCalls.some(args => args[0] === 'checkout')).toBe(true); - expect(harness.captureRequests).toEqual([]); expect(harness.stamp()?.allocationId).toBe('alloc-current'); expect(lastFrame(harness.frames)).toMatchObject({ workspace: 'cloned' }); }); }); -describe('refreshing an adopted repository snapshot', () => { - function refreshHarness(stamp: WorkspaceStamp) { - const harness = createHarness(FAST_TIMERS, { hasGit: true, capture: true }); - harness.setStamp(stamp); - harness.setGit(args => { - if (args[0] === 'show-ref') return result(1); - if (args[0] === 'for-each-ref') return { stdout: 'main\n', stderr: '', exitCode: 0 }; - if (args[0] === 'rev-parse') return { stdout: 'new-commit\n', stderr: '', exitCode: 0 }; - return result(0); - }); - const spec = routeSpec({ - git: { url: REPO, token: 'git-1', platform: 'github' }, - setupCommands: ['npm install'], - capture: true, - }); - return { harness, spec }; - } - - it('captures an adopted snapshot that is due, as the next generation', async () => { - const { harness, spec } = refreshHarness({ ...DUE_STAMP, generation: 2 }); - - await harness.manager.prepare(spec); - - expect(harness.gitCalls.some(args => args[0] === 'clone')).toBe(false); - expect(harness.captureRequests).toHaveLength(1); - expect(progressSteps(harness.frames)).toContain('snapshot'); - expect(harness.stamp()).toEqual({ - allocationId: 'alloc-current', - commit: 'new-commit', - capturedAt: NOW, - generation: 3, - }); - expect(lastFrame(harness.frames)).toMatchObject({ workspace: 'adopted' }); - }); - - it('does not capture a fresh snapshot and keeps its capture time', async () => { - const { harness, spec } = refreshHarness(FOREIGN_STAMP); - - await harness.manager.prepare(spec); - - expect(harness.captureRequests).toEqual([]); - expect(harness.stamp()).toMatchObject({ - capturedAt: FOREIGN_STAMP.capturedAt, - generation: 0, - }); - }); - - it('rebuilds a due snapshot at the generation cap from a clone, as generation 0', async () => { - const { harness, spec } = refreshHarness({ - ...DUE_STAMP, - generation: SNAPSHOT_MAX_GENERATION, - }); - - await harness.manager.prepare(spec); - - expect(harness.emptyCalls).toHaveLength(1); - expect(harness.gitCalls.filter(args => args[0] === 'clone')).toHaveLength(1); - expect(harness.gitCalls.some(args => args[0] === 'fetch')).toBe(false); - expect(harness.captureRequests).toHaveLength(1); - expect(harness.stamp()).toMatchObject({ capturedAt: NOW, generation: 0 }); - expect(lastFrame(harness.frames)).toMatchObject({ workspace: 'cloned' }); - }); - - it('adopts a snapshot at the cap that is not due, without capturing it', async () => { - const { harness, spec } = refreshHarness({ - ...FOREIGN_STAMP, - generation: SNAPSHOT_MAX_GENERATION, - }); - - await harness.manager.prepare(spec); - - expect(harness.emptyCalls).toEqual([]); - expect(harness.captureRequests).toEqual([]); - expect(lastFrame(harness.frames)).toMatchObject({ workspace: 'adopted' }); - expect(harness.stamp()).toMatchObject({ generation: SNAPSHOT_MAX_GENERATION }); - }); - - it('captures a due snapshot only when the route asks for a capture', async () => { - const { harness, spec } = refreshHarness(DUE_STAMP); - - await harness.manager.prepare({ ...spec, capture: undefined }); - - expect(harness.captureRequests).toEqual([]); - }); - - it('starts over at generation 0 when the adopt falls back to a clone', async () => { - const harness = createHarness(timers({ restoreMs: 30, cloneMs: 2_000 }), { - hasGit: true, - capture: true, - }); - harness.setStamp({ ...DUE_STAMP, generation: 3 }); - harness.setGit(args => { - if (args[0] === 'fetch') return new Promise(() => undefined); - return result(args[0] === 'show-ref' ? 1 : 0); - }); - - await harness.manager.prepare( - routeSpec({ git: { url: REPO, token: 'git-1', platform: 'github' }, capture: true }) - ); - - expect(lastFrame(harness.frames)).toMatchObject({ workspace: 'cloned' }); - expect(harness.captureRequests).toHaveLength(1); - expect(harness.stamp()).toMatchObject({ capturedAt: NOW, generation: 0 }); - }); -}); - -describe('capturing a repository snapshot', () => { - function captureSpec(overrides: Partial = {}) { - return routeSpec({ - git: { url: REPO, token: 'git-1', platform: 'github' }, - setupCommands: ['npm install'], - capture: true, - ...overrides, - }); - } - - it('makes origin bare around the capture and restores the credential before Kilo starts', async () => { - const harness = createHarness(FAST_TIMERS, { capture: true }); - const order: string[] = []; - harness.setGit(args => { - if (args[0] === 'remote') { - order.push(args[3]?.includes('git-1') ? 'origin:authenticated' : 'origin:bare'); - } - if (args[0] === 'reflog') order.push('reflog'); - if (args[0] === 'rev-parse') return { stdout: 'abc123\n', stderr: '', exitCode: 0 }; - return result(0); - }); - const spec = captureSpec(); - - await harness.manager.prepare(spec); - - expect(harness.captureRequests).toEqual([ - { sessionId: spec.sessionId, commit: 'abc123', timeoutMs: FAST_TIMERS.wrapper.captureMs }, - ]); - expect(order).toEqual(['origin:bare', 'reflog', 'origin:authenticated']); - expect(progressSteps(harness.frames)).toEqual([ - 'clone', - 'checkout', - 'setup', - 'snapshot', - 'kilo_runtime', - 'kilo_session', - ]); - expect(harness.stamp()).toEqual({ - allocationId: 'alloc-current', - commit: 'abc123', - capturedAt: NOW, - generation: 0, - }); - expect(harness.truncations()).toBe(1); - expect(harness.clearedHomes).toEqual([]); - expect(lastFrame(harness.frames)).toMatchObject({ workspace: 'cloned' }); - }); - - it('writes the stamp before it asks for the capture', async () => { - const harness = createHarness(FAST_TIMERS, { capture: true }); - let stampAtCapture: unknown = 'unset'; - const requests = harness.captureRequests; - const push = requests.push.bind(requests); - requests.push = (...items) => { - stampAtCapture = harness.stamp(); - return push(...items); - }; - - await harness.manager.prepare(captureSpec()); - - expect(stampAtCapture).toMatchObject({ allocationId: 'alloc-current' }); - }); - - it('continues when the capture is not saved or the channel fails, and restores origin', async () => { - for (const outcome of [false, 'throw'] as const) { - const harness = createHarness(FAST_TIMERS, { capture: true }); - harness.setCaptureResult(outcome); - const spec = captureSpec(); - - await harness.manager.prepare(spec); - - expect(lastFrame(harness.frames)).toMatchObject({ type: 'session.ready' }); - const setUrls = harness.gitCalls.filter( - args => args[0] === 'remote' && args[1] === 'set-url' - ); - expect(setUrls.at(-1)?.[3]).toContain('git-1'); - } - }); - - it('does not capture when origin cannot be made bare', async () => { - const harness = createHarness(FAST_TIMERS, { capture: true }); - harness.setGit(args => - args[0] === 'remote' && !args[3]?.includes('git-1') - ? result(1, 'fatal: no such remote') - : result(0) - ); - - await harness.manager.prepare(captureSpec()); - - expect(harness.captureRequests).toEqual([]); - expect(lastFrame(harness.frames)).toMatchObject({ type: 'session.ready' }); - }); - - it('continues through Kilo startup when the capture acknowledgement times out', async () => { - const capture = createWorkspaceCapture({ send: () => undefined }); - const harness = createHarness(timers({ captureMs: 20 }), { - capture: true, - captureRequest: (sessionId, commit, timeoutMs) => - capture.request(sessionId, commit, timeoutMs), - }); - const spec = captureSpec(); - harness.setSessionExists(false); - - await harness.manager.prepare(spec); - - expect(harness.captureRequests).toHaveLength(1); - expect(harness.logs).toContain(`control-plane capture not saved session=${spec.sessionId}`); - const setUrls = harness.gitCalls.filter(args => args[0] === 'remote' && args[1] === 'set-url'); - expect(setUrls.at(-1)?.[3]).toContain('git-1'); - expect(harness.ensureCalls()).toBe(1); - expect(harness.ensureSessionCalls()).toBe(1); - expect(lastFrame(harness.frames)).toMatchObject({ type: 'session.ready' }); - expect(harness.frames.some(frame => frame.type === 'session.failed')).toBe(false); - }); - - it('fails the route when the credential cannot be restored after a capture', async () => { - const harness = createHarness(FAST_TIMERS, { capture: true }); - harness.setGit(args => - args[0] === 'remote' && args[3]?.includes('git-1') - ? result(1, 'fatal: cannot set url') - : result(0) - ); - - await harness.manager.prepare(captureSpec()); - - expect(lastFrame(harness.frames)).toMatchObject({ - type: 'session.failed', - reason: 'workspace_setup_failed', - step: 'snapshot', - }); - }); - - it('does not capture without a capture request, a repository or a capture channel', async () => { - const withoutRequest = createHarness(FAST_TIMERS, { capture: true }); - await withoutRequest.manager.prepare(captureSpec({ capture: undefined })); - expect(withoutRequest.captureRequests).toEqual([]); - - const withoutChannel = createHarness(FAST_TIMERS); - await withoutChannel.manager.prepare(captureSpec()); - expect(progressSteps(withoutChannel.frames)).not.toContain('snapshot'); - - const withoutRepo = createHarness(FAST_TIMERS, { capture: true }); - await withoutRepo.manager.prepare(captureSpec({ git: undefined })); - expect(withoutRepo.captureRequests).toEqual([]); - }); -}); - describe('control-plane runtime profiles', () => { it('installs the trusted GitHub publisher and retains its target and denies on credential refresh', async () => { const harness = createHarness(FAST_TIMERS); @@ -3386,7 +3467,7 @@ describe('release cleanup with no live runtime client', () => { release: () => undefined, }, inheritedEnv: {}, - homeRoot: '/tmp/prepare-test-homes', + homeRoots: homeRootsFor('/tmp/prepare-test-homes'), hasGit: async () => false, readStamp: async () => null, writeStamp: async () => undefined, diff --git a/services/cloud-agent-next/wrapper/src/control-plane/prepare.ts b/services/cloud-agent-next/wrapper/src/control-plane/prepare.ts index 86ddf79420c..322469706b1 100644 --- a/services/cloud-agent-next/wrapper/src/control-plane/prepare.ts +++ b/services/cloud-agent-next/wrapper/src/control-plane/prepare.ts @@ -1,23 +1,17 @@ import { runSetupScript } from '../setup-script.js'; import { materializeRuntimeProfile, withRuntimeProfileEnvironment } from './runtime-profile.js'; -import { createHash } from 'node:crypto'; import fs from 'node:fs/promises'; -import os from 'node:os'; import path from 'node:path'; import { CONTROL_PLANE_PREPARATION_DETAIL_MAX_LENGTH, - CONTROL_PLANE_SETUP_EVENTS, type ControlPlanePreparationStep, type ControlPlaneRouteSpec, type ControlPlaneSessionCredentialsPayload, - type ControlPlaneSetupEvent, type ControlPlaneWorkspaceOutcome, type ControlPlaneWrapperFrame, } from '../../../src/shared/control-plane-protocol.js'; import type { ControlPlaneTimers } from '../../../src/shared/control-plane-timers.js'; import type { ControlDiagnosticReporter } from '../../../src/shared/control-diagnostics.js'; -import { parseControlPlaneCredential } from '../../../src/shared/control-plane-credential.js'; -import type { WorkspaceFailureSubtype } from '../../../src/shared/wrapper-bootstrap.js'; import type { WrapperKiloClient } from '../kilo-api.js'; import { fetchKiloServer } from '../kilo-server-auth.js'; import { @@ -29,60 +23,41 @@ import { type ProcessSpawn, } from '../utils.js'; import { WrapperBootstrapError } from '../bootstrap-error.js'; -import { - cleanTerminalOutput, - formatGitResultFailure, - gitOperationError, - type GitRouteClass, -} from '../git-errors.js'; +import { formatGitResultFailure, gitOperationError, type GitRouteClass } from '../git-errors.js'; import { authenticatedGitUrl } from '../control/git-url.js'; import { checkoutSyntheticReviewRef, isSyntheticReviewRef } from '../git-review-ref.js'; import { buildWorktreeKiloEnvironment, type WorktreeKiloAuth, } from '../control/worktree-runtime.js'; -import { createOutputRedactor, createSecretRedactor, redactSecrets } from '../redact-output.js'; +import { createSecretRedactor, redactSecrets } from '../redact-output.js'; import { KiloWorktreeCredentialClassMismatchError, KiloWorktreeMcpMismatchError, } from './kilo-runtime.js'; -import { - configureWorkspaceGitAuthor, - createGitProgressReporter, - isBloblessReviewCloneEligible, -} from '../session-bootstrap.js'; +import { configureWorkspaceGitAuthor, createGitProgressReporter } from '../session-bootstrap.js'; import { restoreSession, seedSessionIngestRegistration } from '../restore-session.js'; import { reportRestoreIncomplete } from '../restore-incomplete.js'; import { restoreWorktreeRecovery } from './worktree-recovery.js'; import type { ControlWorkload } from '../control/workload-cgroup.js'; -import { - emptyWorkspaceDirectory, - refreshAdoptedRepository, - removeStaleHomes, - stripGitCredentials, - truncateWrapperLog, -} from './workspace-adoption.js'; -import type { WorkspaceCapture } from './workspace-capture.js'; +import { emptyWorkspaceDirectory, refreshAdoptedRepository } from './workspace-adoption.js'; import { classifyWorkspace, - planStamp, - snapshotAction, readWorkspaceStamp, writeWorkspaceStamp, type WorkspaceInspection, type WorkspaceStamp, } from './workspace-stamp.js'; +import { + SETUP_COMMAND_HARD_TIMEOUT_MS, + SETUP_COMMAND_INACTIVITY_TIMEOUT_MS, + createRepositoryPreparation, + managedGitHubPreparationGitArgs, +} from './repository-preparation.js'; +import { defaultHomeRoots, routeHome, type HomeRoots } from './workspace-home.js'; -/** Spec §7 "Setup commands": current per-command limits. */ -const SETUP_COMMAND_INACTIVITY_TIMEOUT_MS = 5 * 60_000; -const SETUP_COMMAND_HARD_TIMEOUT_MS = 8 * 60_000; -/** Spec §7 "Clone or fetch": network errors get 3 attempts with backoff. */ -const CLONE_RETRY_ATTEMPTS = 3; -const CLONE_RETRY_BACKOFF_MS = [1_000, 2_000]; /** Spec §7: Kilo runtime start and Kilo session each get one retry. */ const STEP_RETRY_ATTEMPTS = 2; -/** Upper bound on one setup-output event so a chatty command cannot flood the wire. */ -const SETUP_OUTPUT_EVENT_LIMIT = 8_192; /** * Release cleanup bounds. Aborting a released route's Kilo session tree is best * effort: it must terminate and never block the route release, so the walk is @@ -93,53 +68,6 @@ const RELEASE_CLEANUP_MAX_NODES = 64; const RELEASE_CLEANUP_DEADLINE_MS = 10_000; const RELEASE_CLEANUP_ATTEMPTS = 2; -/** - * Spec §7: eligible managed GitHub HTTPS preparation clone/fetch opts into the - * two invocation-scoped native Git options together. `proactiveAuth=basic` - * makes the first request carry the existing URL-bound control alias so - * contained resolution, repository-authorized redemption and the bounded - * Retry-After handler run instead of an anonymous first request; - * `followRedirects=false` fails every redirect (including a same-origin one) - * rather than letting Git reattach the alias to a redirected request. Never one - * option without the other, and never on an ineligible command. - */ -const MANAGED_GITHUB_PREPARATION_GIT_CONFIG = [ - '-c', - 'http.https://github.com/.proactiveAuth=basic', - '-c', - 'http.https://github.com/.followRedirects=false', -] as const; - -/** - * The single eligibility decision for the two invocation-scoped options. Both - * options or neither: the command must be preparation clone/fetch (or a review - * checkout that can lazily fetch blobs), the platform - * must be managed GitHub, the token must parse as a `github` control alias, and - * the URL must be the direct HTTPS default-port github.com URL. Callers judge - * `spec.git.url`, never the password parsed back out of the authenticated URL. - */ -function managedGitHubPreparationGitArgs(spec: ControlPlaneRouteSpec, command: string): string[] { - if ( - command !== 'clone' && - command !== 'fetch' && - !(command === 'checkout' && spec.createdOnPlatform === 'code-review') - ) - return []; - const git = spec.git; - if (!git || git.platform !== 'github') return []; - if (!git.token || parseControlPlaneCredential(git.token)?.purpose !== 'github') return []; - let url: URL; - try { - url = new URL(git.url); - } catch { - return []; - } - // WHATWG `URL.port` is '' for both `https://github.com/...` and an explicit - // `:443`, so explicit 443 is eligible and only a non-empty port is excluded. - if (url.protocol !== 'https:' || url.hostname !== 'github.com' || url.port !== '') return []; - return [...MANAGED_GITHUB_PREPARATION_GIT_CONFIG]; -} - export type PrepareRuntimePort = { ensure(input: { key: string; @@ -169,8 +97,6 @@ export type PrepareDeps = { runtimes: PrepareRuntimePort; /** This wrapper's allocation; a workspace stamped by another one came from a snapshot. */ allocationId: string; - /** Asks the Sandbox DO to snapshot the container; absent disables capture. */ - capture?: Pick; log?: (message: string) => void; onNativeDiagnostic?: ControlDiagnosticReporter; runGit?: (args: string[], options?: ProcessOptions) => Promise; @@ -198,13 +124,9 @@ export type PrepareDeps = { readStamp?: (directory: string) => Promise; writeStamp?: (directory: string, stamp: WorkspaceStamp) => Promise; emptyDirectory?: (directory: string) => Promise; - /** Removes the homes a restored snapshot carries, keeping this route's own. */ - clearStaleHomes?: (homeRoot: string, keep: string) => Promise; - truncateLog?: () => Promise; sleep?: (ms: number, signal: AbortSignal) => Promise; - now?: () => number; inheritedEnv?: NodeJS.ProcessEnv; - homeRoot?: string; + homeRoots?: HomeRoots; /** Bounds release cleanup; defaults to `RELEASE_CLEANUP_DEADLINE_MS`. */ releaseCleanupDeadlineMs?: number; }; @@ -297,21 +219,6 @@ export function runtimeKey(spec: ControlPlaneRouteSpec): string { return spec.runtimeIsolation === 'per-session' ? spec.sessionId : spec.directory; } -function homeKey(spec: ControlPlaneRouteSpec): string { - return spec.runtimeIsolation === 'per-session' - ? spec.sessionId - : (spec.kilo?.scopeId ?? spec.sessionId); -} - -function homeFor(key: string, directory: string, homeRoot: string): string { - const id = createHash('sha256').update(key).update('\0').update(directory).digest('hex'); - return path.join(homeRoot, id); -} - -function isNetworkFailure(subtype: WorkspaceFailureSubtype | undefined): boolean { - return subtype === 'git_network_failed' || subtype === 'git_rate_limited'; -} - function isTimeoutMessage(step: ControlPlanePreparationStep, error: unknown): boolean { return error instanceof Error && error.message === `${step} timed out`; } @@ -445,8 +352,7 @@ export function createPreparationManager(deps: PrepareDeps): PreparationManager const configureGitAuthor = deps.configureGitAuthor ?? configureWorkspaceGitAuthor; const sessionExists = deps.sessionExists ?? defaultSessionExists; const sleep = deps.sleep ?? defaultSleep; - const now = deps.now ?? Date.now; - const homeRoot = deps.homeRoot ?? path.join(os.tmpdir(), 'kilo-worktrees'); + const homeRoots = deps.homeRoots ?? defaultHomeRoots(); const prepared = new Map(); const preparing = new Map(); const releaseCleanups = new Map(); @@ -465,9 +371,9 @@ export function createPreparationManager(deps: PrepareDeps): PreparationManager (async (directory: string, stamp: WorkspaceStamp) => writeWorkspaceStamp(directory, await hasGit(directory), stamp)); const emptyDirectory = deps.emptyDirectory ?? emptyWorkspaceDirectory; - const clearStaleHomes = deps.clearStaleHomes ?? removeStaleHomes; - const truncateLog = deps.truncateLog ?? (() => truncateWrapperLog(inheritedEnv.WRAPPER_LOG_PATH)); const mkdir = deps.mkdir ?? (dir => fs.mkdir(dir, { recursive: true }).then(() => undefined)); + const repository = createRepositoryPreparation({ runGit, runSetup, sleep, mkdir, hasGit, log }); + const { runNetworkGit, headCommit } = repository; function emitProgress( sessionId: string, @@ -579,131 +485,6 @@ export function createPreparationManager(deps: PrepareDeps): PreparationManager throw lastError; } - /** Runs a git command that talks to the remote; a network failure retries with backoff. */ - async function runNetworkGit( - args: string[], - options: ProcessOptions, - redact: (text: string) => string, - signal: AbortSignal - ): Promise { - let last: ExecResult | undefined; - for (let attempt = 1; attempt <= CLONE_RETRY_ATTEMPTS; attempt += 1) { - signal.throwIfAborted(); - last = await runGit(args, options); - if (last.exitCode === 0) return last; - const failure = gitOperationError(last, 'clone', redact); - if (!isNetworkFailure(failure.subtype) || attempt === CLONE_RETRY_ATTEMPTS) return last; - await sleep(CLONE_RETRY_BACKOFF_MS[attempt - 1] ?? 0, signal); - } - return last as ExecResult; - } - - async function cloneRepository( - spec: ControlPlaneRouteSpec, - directory: string, - env: Record, - redact: (text: string) => string, - signal: AbortSignal - ): Promise { - emitProgress(spec.sessionId, 'clone'); - await mkdir(directory); - if (!spec.git || (await hasGit(directory))) return; - // Diagnostic route class only: a managed credential was injected into the - // clone URL, otherwise the plain (direct) URL is used. Never log the URL. - const gitRoute: GitRouteClass = spec.git.token ? 'managed' : 'direct'; - if (!(await hasGit(directory))) { - const cloneUrl = authenticatedGitUrl(spec.git.url, spec.git.token, spec.git.platform); - const gitConfigArgs = managedGitHubPreparationGitArgs(spec, 'clone'); - const useBlobless = isBloblessReviewCloneEligible( - spec.createdOnPlatform, - spec.git.platform, - spec.git.token - ); - const noCheckout = - spec.createdOnPlatform === 'code-review' && - spec.branch !== undefined && - isSyntheticReviewRef(spec.branch) && - spec.branchMode !== 'working'; - const startedAt = Date.now(); - let cloneAttempts = 0; - let filterRejected = false; - const runClone = async (): Promise => { - signal.throwIfAborted(); - cloneAttempts += 1; - const cloned = await runGit( - [ - ...gitConfigArgs, - 'clone', - '--progress', - ...(useBlobless && !filterRejected ? ['--filter=blob:none'] : []), - ...(noCheckout ? ['--no-checkout'] : []), - cloneUrl, - directory, - ], - { - env, - inheritEnv: false, - signal, - onOutput: createGitProgressReporter(progressText => - emitProgress(spec.sessionId, 'clone', `Cloning repository... ${progressText}`) - ), - } - ); - signal.throwIfAborted(); - return cloned; - }; - let lastError: WrapperBootstrapError | undefined; - for (let attempt = 1; attempt <= CLONE_RETRY_ATTEMPTS; attempt += 1) { - signal.throwIfAborted(); - if (attempt > 1) { - emitProgress( - spec.sessionId, - 'clone', - `Retrying clone (attempt ${attempt} of ${CLONE_RETRY_ATTEMPTS})` - ); - } - let cloned = await runClone(); - if ( - useBlobless && - !filterRejected && - cloned.exitCode !== 0 && - !cloned.terminationReason && - /(?:filter(?:ing)?[^\r\n]*(?:not supported|not recognized|rejected)|(?:does not support|unsupported|invalid)[^\r\n]*filter)/i.test( - `${cloned.stderr}\n${cloned.stdout}` - ) - ) { - filterRejected = true; - log( - `control-plane blobless clone rejected; retrying full clone session=${spec.sessionId}` - ); - cloned = await runClone(); - } - if (cloned.exitCode === 0) { - lastError = undefined; - break; - } - lastError = gitOperationError(cloned, 'clone', redact, gitRoute); - if (!isNetworkFailure(lastError.subtype) || attempt === CLONE_RETRY_ATTEMPTS) break; - await sleep(CLONE_RETRY_BACKOFF_MS[attempt - 1] ?? 0, signal); - } - if (lastError) throw lastError; - let filterHonored = false; - if (useBlobless && !filterRejected) { - const promisor = await runGit(['config', '--get', 'remote.origin.promisor'], { - cwd: directory, - env, - inheritEnv: false, - signal, - }); - filterHonored = promisor.exitCode === 0 && promisor.stdout.trim() === 'true'; - } - const mode = filterRejected ? 'blobless_fallback' : filterHonored ? 'blobless' : 'full'; - log( - `control-plane clone complete session=${spec.sessionId} mode=${mode} attempts=${cloneAttempts} durationMs=${Date.now() - startedAt}` - ); - } - } - async function cloneWorkspace( spec: ControlPlaneRouteSpec, directory: string, @@ -712,7 +493,18 @@ export function createPreparationManager(deps: PrepareDeps): PreparationManager signal: AbortSignal, onCheckout: () => void ): Promise { - await cloneRepository(spec, directory, env, redact, signal); + await repository.cloneRepository({ + git: spec.git, + sessionId: spec.sessionId, + createdOnPlatform: spec.createdOnPlatform, + branch: spec.branch, + branchMode: spec.branchMode, + directory, + env, + redact, + signal, + onProgress: detail => emitProgress(spec.sessionId, 'clone', detail), + }); if (!spec.git) return; onCheckout(); await checkoutWorkspace(spec, directory, env, redact, signal); @@ -881,113 +673,37 @@ export function createPreparationManager(deps: PrepareDeps): PreparationManager return { inspection, stamp }; } - async function headCommit(directory: string, env: Record): Promise { - const result = await runGit(['rev-parse', 'HEAD'], { - cwd: directory, - env, - inheritEnv: false, - }); - return result.exitCode === 0 ? result.stdout.trim() : ''; - } - - /** - * Save the prepared workspace as a repository snapshot. `origin` is bare while the - * Sandbox DO snapshots the container, and gets this route's credential back - * whatever happens; a failed capture is logged and preparation continues. - */ - async function captureWorkspace( - spec: ControlPlaneRouteSpec, - directory: string, - env: Record, - commit: string - ): Promise { - const git = spec.git; - const capture = deps.capture; - if (!git || !capture) return; - emitProgress(spec.sessionId, 'snapshot'); - await truncateLog().catch(() => undefined); - const runWorkspaceGit = (args: string[]): Promise => - runGit(args, { cwd: directory, env, inheritEnv: false }); - try { - const bare = await stripGitCredentials({ - git: runWorkspaceGit, - directory, - bareUrl: git.url, - }); - if (!bare) { - log(`control-plane capture skipped, origin not bare session=${spec.sessionId}`); - return; - } - const saved = await capture.request(spec.sessionId, commit, timers.captureMs); - log(`control-plane capture ${saved ? 'saved' : 'not saved'} session=${spec.sessionId}`); - } catch (error) { - log( - `control-plane capture failed session=${spec.sessionId} error=${ - error instanceof Error ? createSecretRedactor(env)(error.message) : 'unknown' - }` - ); - } finally { - await restoreOriginCredential(spec, runWorkspaceGit); - } - } - - async function restoreOriginCredential( - spec: ControlPlaneRouteSpec, - run: (args: string[]) => Promise - ): Promise { - const git = spec.git; - if (!git) return; - const url = authenticatedGitUrl(git.url, git.token, git.platform); - for (let attempt = 1; attempt <= STEP_RETRY_ATTEMPTS; attempt += 1) { - const result = await run(['remote', 'set-url', 'origin', url]); - if (result.exitCode === 0) return; - } - throw new WrapperBootstrapError({ - code: 'WORKSPACE_SETUP_FAILED', - message: 'Failed to restore the repository credential after a snapshot', - retryable: true, - }); - } - /** * Bring the route's directory to a prepared workspace and report how. The * filesystem alone decides (spec: workspace stamp): a stamp from this allocation - * needs nothing, one from another allocation is a restored snapshot to adopt, - * and anything else is cloned. Setup always runs after a clone or an adopt. A - * clone is captured as a new snapshot; an adopted snapshot only once it is old, - * so the refresh builds on it instead of starting from scratch, until the - * generation cap sends it back to a clone. + * needs nothing, one from another allocation is a restored environment snapshot + * to adopt, and anything else is cloned. Setup always runs after a clone or an + * adopt, with this session's admitted configuration. A session never captures. */ async function prepareWorkspace( spec: ControlPlaneRouteSpec, directory: string, env: Record, redact: (text: string) => string, - home: string, setStep: (step: ControlPlanePreparationStep) => void, - onReplacementWorkspace?: () => Promise - ): Promise { + onReplacementWorkspace?: () => Promise + ): Promise<{ + outcome: ControlPlaneWorkspaceOutcome; + deferredStamp: { commit: string } | null; + }> { const workspaceStartedAt = Date.now(); const inspectStartedAt = Date.now(); - const { inspection, stamp: previous } = await inspectWorkspace(directory); + const { inspection } = await inspectWorkspace(directory); log( `control-plane workspace inspect session=${spec.sessionId} inspection=${inspection} elapsedMs=${Date.now() - inspectStartedAt}` ); - if (inspection === 'same') return 'same'; + if (inspection === 'same') return { outcome: 'same', deferredStamp: null }; let adopted = false; if (inspection === 'foreign') { - const clearStartedAt = Date.now(); - await clearStaleHomes(homeRoot, home).catch(() => undefined); - log( - `control-plane workspace stale homes cleared session=${spec.sessionId} elapsedMs=${Date.now() - clearStartedAt}` - ); - // A snapshot at the generation cap that is due again is not adopted: it is - // rebuilt from a clone, so the layers stacked on it do not accumulate. - const rebuild = previous !== null && snapshotAction(previous, now()) === 'rebuild'; const adoptStartedAt = Date.now(); - adopted = !rebuild && (await adoptWorkspace(spec, directory, env, redact, setStep)); + adopted = await adoptWorkspace(spec, directory, env, redact, setStep); log( - `control-plane workspace adopt done session=${spec.sessionId} adopted=${adopted} rebuild=${rebuild} elapsedMs=${Date.now() - adoptStartedAt}` + `control-plane workspace adopt done session=${spec.sessionId} adopted=${adopted} elapsedMs=${Date.now() - adoptStartedAt}` ); if (!adopted) await emptyDirectory(directory); } @@ -1002,20 +718,25 @@ export function createPreparationManager(deps: PrepareDeps): PreparationManager } setStep('setup'); await runSetupCommands(spec, directory, env, redact, new AbortController().signal); - if (cloning || adopted) await onReplacementWorkspace?.(); + // The callback reports whether the recovery steps actually applied. A clone + // with no `.git` yet, or a surviving workspace, never runs it. + const recoveryApplied = + cloning || adopted ? ((await onReplacementWorkspace?.()) ?? false) : false; const commit = await headCommit(directory, env); - // The stamp goes down before the snapshot, so a container restored from it - // sees another allocation's stamp and adopts rather than trusting the files. - const plan = planStamp({ cloning, adopted, previous, now: now() }); - await writeStamp(directory, { allocationId: deps.allocationId, commit, ...plan.lineage }); - if (plan.capture && spec.capture === true) { - setStep('snapshot'); - await captureWorkspace(spec, directory, env, commit); + // Recovery requested but not applied: defer the current-allocation stamp until + // after the deferred import, so a crash in between cannot leave a stamp that + // makes the next preparation look like a surviving workspace. + if (spec.worktreeRecovery !== undefined && !recoveryApplied) { + log( + `control-plane workspace stamp deferred session=${spec.sessionId} elapsedMs=${Date.now() - workspaceStartedAt}` + ); + return { outcome: adopted ? 'adopted' : 'cloned', deferredStamp: { commit } }; } + await writeStamp(directory, { allocationId: deps.allocationId, commit }); log( `control-plane workspace prepared session=${spec.sessionId} outcome=${adopted ? 'adopted' : 'cloned'} elapsedMs=${Date.now() - workspaceStartedAt}` ); - return adopted ? 'adopted' : 'cloned'; + return { outcome: adopted ? 'adopted' : 'cloned', deferredStamp: null }; } async function runSetupCommands( @@ -1028,64 +749,17 @@ export function createPreparationManager(deps: PrepareDeps): PreparationManager const commands = spec.setupCommands ?? []; if (commands.length === 0) return; emitProgress(spec.sessionId, 'setup'); - // Spec §9: a setup failure must show the command output, so each command's - // start, output and end go on the wire, not only to the wrapper log. The - // Session renders them as per-command preparation steps. - const emitSetupEvent = (event: ControlPlaneSetupEvent): void => { - deps.emit({ - type: 'session.events', - sessionId: spec.sessionId, - events: [event], - }); - }; - for (const [index, command] of commands.entries()) { - signal.throwIfAborted(); - const commandNumber = index + 1; - emitSetupEvent({ - type: CONTROL_PLANE_SETUP_EVENTS.started, - properties: { command: commandNumber, commandCount: commands.length }, - }); - const output = createOutputRedactor( - text => redact(cleanTerminalOutput(text)), - text => { - if (signal.aborted) return; - const cleaned = text.trim(); - if (!cleaned) return; - log(`control-plane setup command ${commandNumber} produced output`); - emitSetupEvent({ - type: CONTROL_PLANE_SETUP_EVENTS.output, - properties: { - command: commandNumber, - output: `${cleaned.slice(0, SETUP_OUTPUT_EVENT_LIMIT - 1)}\n`, - }, - }); - } - ); - const startedAt = Date.now(); - const result = await runSetup(command, directory, env, output.onOutput, signal); - output.flush(); - if (result.exitCode !== 0) { - const timedOut = result.terminationReason !== undefined; - log( - `control-plane setup command failed sessionId=${spec.sessionId} kiloSessionId=${spec.kiloSessionId} attemptId=${spec.attemptId} index=${index + 1} count=${commands.length} exitCode=${result.exitCode} terminationReason=${result.terminationReason ?? 'nonzero'} elapsedMs=${Date.now() - startedAt} inactivityTimeoutMs=${SETUP_COMMAND_INACTIVITY_TIMEOUT_MS} hardTimeoutMs=${SETUP_COMMAND_HARD_TIMEOUT_MS}` - ); - const message = `Setup command ${commandNumber} ${timedOut ? 'timed out' : 'failed'}`; - emitSetupEvent({ - type: CONTROL_PLANE_SETUP_EVENTS.finished, - properties: { command: commandNumber, exitCode: result.exitCode, safeError: message }, - }); - throw new WrapperBootstrapError({ - code: 'WORKSPACE_SETUP_FAILED', - subtype: timedOut ? 'setup_command_timeout' : 'setup_command_failed', - message, - retryable: true, - }); - } - emitSetupEvent({ - type: CONTROL_PLANE_SETUP_EVENTS.finished, - properties: { command: commandNumber, exitCode: 0 }, - }); - } + // The Session renders the setup events as per-command preparation steps. + await repository.runSetupCommands({ + commands, + directory, + env, + redact, + signal, + onEvent: event => + deps.emit({ type: 'session.events', sessionId: spec.sessionId, events: [event] }), + logContext: `sessionId=${spec.sessionId} kiloSessionId=${spec.kiloSessionId} attemptId=${spec.attemptId}`, + }); } async function resolveKiloSession( @@ -1283,7 +957,7 @@ export function createPreparationManager(deps: PrepareDeps): PreparationManager : { organizationId: kiloConfig.organizationId }), targets: credentials?.proxy?.targets ?? kiloConfig.targets, }; - const home = homeFor(homeKey(spec), directory, homeRoot); + const home = routeHome(spec, homeRoots); log(`control-plane prepare received session=${sessionId} directory=${directory}`); try { @@ -1323,21 +997,55 @@ export function createPreparationManager(deps: PrepareDeps): PreparationManager let workspace: ControlPlaneWorkspaceOutcome | undefined; let replacementWorkspace = false; let localRecoveryApplied = false; + let deferredRuntimeStarted = false; const recovery = spec.worktreeRecovery; + // Older ingest restoration must not overwrite newer local recovery. That is + // proven for a replacement workspace whose recovery applied, and for a + // surviving workspace this allocation already stamped (a wrapper restart + // after the stamp but before import). An unstamped repository is a partial + // preparation, not proof of recovery, so it keeps ingest restoration. + const restoreFilesFor = (): boolean => + !(replacementWorkspace && localRecoveryApplied) && + !(recovery !== undefined && workspace === 'same'); + // One owner for runtime profile, runtime start and session resolution, shared + // by the normal path after the lock and the deferred path inside it. + const ensureRuntimeAndSession = async (restoreFiles: boolean): Promise => { + currentStep = 'kilo_runtime'; + emitProgress(sessionId, 'kilo_runtime'); + await materializeRuntimeProfile(home, spec); + const client = await withRuntimeStartRetry( + key, + () => + withinStep('kilo_runtime', timers.kiloRuntimeStartMs, () => + deps.runtimes.ensure({ key, directory, env: builtEnv }) + ), + () => !owner.released + ); + if (owner.released) return; + currentStep = 'kilo_session'; + emitProgress(sessionId, 'kilo_session'); + await settleReleaseCleanup(sessionId); + await withOneRetry( + () => + withinStep('kilo_session', timers.kiloSessionMs, signal => + resolveKiloSession(spec, directory, builtEnv, client, signal, restoreFiles) + ), + () => !owner.released + ); + }; await withWorkspaceLock(directory, async () => { if (!needsWorkspace) return; - workspace = await prepareWorkspace( + const preparedWorkspace = await prepareWorkspace( spec, directory, builtEnv, redact, - home, step => { currentStep = step; }, async () => { replacementWorkspace = true; - if (!recovery || owner.released) return; + if (!recovery || owner.released) return false; currentStep = 'restore'; try { emitProgress(sessionId, 'restore', 'Restoring saved worktree changes'); @@ -1357,6 +1065,7 @@ export function createPreparationManager(deps: PrepareDeps): PreparationManager log, }); if (incomplete) emitProgress(sessionId, 'restore', incomplete.message); + return localRecoveryApplied; } catch { log(`local worktree recovery session=${sessionId} failed; continuing`); emitProgress( @@ -1364,45 +1073,34 @@ export function createPreparationManager(deps: PrepareDeps): PreparationManager 'restore', 'Saved worktree changes could not be restored; continuing' ); + return false; } } ); + workspace = preparedWorkspace.outcome; + if (preparedWorkspace.deferredStamp === null || owner.released) return; + // Recovery was requested but did not apply. Import the older ingest while + // the tree is unstamped, then write the same `planStamp` result only after + // the import returns, so a crash in between leaves the tree unstamped. The + // lock keeps siblings from adopting or emptying the directory meanwhile. + deferredRuntimeStarted = true; + await ensureRuntimeAndSession(restoreFilesFor()); + if (!owner.released) { + await writeStamp(directory, { + allocationId: deps.allocationId, + commit: preparedWorkspace.deferredStamp.commit, + }); + } }); - if (owner.released) return; + if (owner.released) { + return; + } if (recovery && !replacementWorkspace) { log(`local worktree recovery session=${sessionId} skipped reason=surviving_workspace`); } - currentStep = 'kilo_runtime'; - emitProgress(sessionId, 'kilo_runtime'); - await materializeRuntimeProfile(home, spec); - const started = await withRuntimeStartRetry( - key, - () => - withinStep('kilo_runtime', timers.kiloRuntimeStartMs, () => - deps.runtimes.ensure({ key, directory, env: builtEnv }) - ), - () => !owner.released - ); - if (owner.released) return; - currentStep = 'kilo_session'; - emitProgress(sessionId, 'kilo_session'); - // A previous release's tree walk may still be running on this runtime; - // stop it before resolving so it cannot abort the new session. - await settleReleaseCleanup(sessionId); - await withOneRetry( - () => - withinStep('kilo_session', timers.kiloSessionMs, signal => - resolveKiloSession( - spec, - directory, - builtEnv, - started, - signal, - !(replacementWorkspace && localRecoveryApplied) - ) - ), - () => !owner.released - ); + if (!deferredRuntimeStarted) { + await ensureRuntimeAndSession(restoreFilesFor()); + } if (owner.released) return; prepared.set(sessionId, { key, diff --git a/services/cloud-agent-next/wrapper/src/control-plane/repository-preparation.ts b/services/cloud-agent-next/wrapper/src/control-plane/repository-preparation.ts new file mode 100644 index 00000000000..bf5f5492b6b --- /dev/null +++ b/services/cloud-agent-next/wrapper/src/control-plane/repository-preparation.ts @@ -0,0 +1,332 @@ +import { + CONTROL_PLANE_SETUP_EVENTS, + type ControlPlaneRouteSpec, + type ControlPlaneSetupEvent, +} from '../../../src/shared/control-plane-protocol.js'; +import { parseControlPlaneCredential } from '../../../src/shared/control-plane-credential.js'; +import type { WorkspaceFailureSubtype } from '../../../src/shared/wrapper-bootstrap.js'; +import { WrapperBootstrapError } from '../bootstrap-error.js'; +import { cleanTerminalOutput, gitOperationError, type GitRouteClass } from '../git-errors.js'; +import { authenticatedGitUrl } from '../control/git-url.js'; +import { createOutputRedactor } from '../redact-output.js'; +import { isSyntheticReviewRef } from '../git-review-ref.js'; +import { isBloblessReviewCloneEligible, createGitProgressReporter } from '../session-bootstrap.js'; +import type { ExecResult, ProcessOptions, ProcessOutputStream } from '../utils.js'; + +/** + * Repository preparation shared by session preparation (`prepare.ts`) and the + * environment builder: clone with bounded network retries, setup commands with + * redacted, bounded output, and the commit a workspace ended at. Session + * concerns (routes, stamps, adoption, Kilo) stay with the session caller; build + * concerns (default-branch pinning, credential cleanup, capture) stay with the + * builder. + */ + +/** Spec §7 "Setup commands": current per-command limits. */ +export const SETUP_COMMAND_INACTIVITY_TIMEOUT_MS = 5 * 60_000; +export const SETUP_COMMAND_HARD_TIMEOUT_MS = 8 * 60_000; +/** Spec §7 "Clone or fetch": network errors get 3 attempts with backoff. */ +const CLONE_RETRY_ATTEMPTS = 3; +const CLONE_RETRY_BACKOFF_MS = [1_000, 2_000]; +/** Upper bound on one setup-output event so a chatty command cannot flood the wire. */ +export const SETUP_OUTPUT_EVENT_LIMIT = 8_192; + +export type RepositoryGit = NonNullable; + +/** + * Spec §7: eligible managed GitHub HTTPS preparation opts into the + * two invocation-scoped native Git options together. `proactiveAuth=basic` + * makes the first request carry the existing URL-bound control alias so + * contained resolution, repository-authorized redemption and the bounded + * Retry-After handler run instead of an anonymous first request; + * `followRedirects=false` fails every redirect (including a same-origin one) + * rather than letting Git reattach the alias to a redirected request. Never one + * option without the other, and never on an ineligible command. + */ +const MANAGED_GITHUB_PREPARATION_GIT_CONFIG = [ + '-c', + 'http.https://github.com/.proactiveAuth=basic', + '-c', + 'http.https://github.com/.followRedirects=false', +] as const; + +/** + * The single eligibility decision for the two invocation-scoped options. Both + * options or neither: the command must clone, fetch, or check out a code review + * that can lazily fetch blobs. The platform + * must be managed GitHub, the token must parse as a `github` control alias, and + * the URL must be the direct HTTPS default-port github.com URL. Callers judge + * the route's git source, never the password parsed back out of the + * authenticated URL. + */ +export function managedGitHubPreparationGitArgs( + spec: Pick, + command: string +): string[] { + if ( + command !== 'clone' && + command !== 'fetch' && + !(command === 'checkout' && spec.createdOnPlatform === 'code-review') + ) + return []; + const git = spec.git; + if (!git || git.platform !== 'github') return []; + if (!git.token || parseControlPlaneCredential(git.token)?.purpose !== 'github') return []; + let url: URL; + try { + url = new URL(git.url); + } catch { + return []; + } + // WHATWG `URL.port` is '' for both `https://github.com/...` and an explicit + // `:443`, so explicit 443 is eligible and only a non-empty port is excluded. + if (url.protocol !== 'https:' || url.hostname !== 'github.com' || url.port !== '') return []; + return [...MANAGED_GITHUB_PREPARATION_GIT_CONFIG]; +} + +function isNetworkFailure(subtype: WorkspaceFailureSubtype | undefined): boolean { + return subtype === 'git_network_failed' || subtype === 'git_rate_limited'; +} + +export type RunSetupCommand = ( + command: string, + directory: string, + env: Record, + onOutput?: (stream: ProcessOutputStream, output: string) => void, + signal?: AbortSignal +) => Promise; + +export type RepositoryPreparationDeps = { + runGit: (args: string[], options?: ProcessOptions) => Promise; + runSetup: RunSetupCommand; + sleep: (ms: number, signal: AbortSignal) => Promise; + mkdir: (directory: string) => Promise; + hasGit: (directory: string) => Promise; + log: (message: string) => void; +}; + +export type CloneInput = { + sessionId?: string; + createdOnPlatform?: string; + branch?: string; + branchMode?: ControlPlaneRouteSpec['branchMode']; + git: RepositoryGit | undefined; + directory: string; + env: Record; + redact: (text: string) => string; + signal: AbortSignal; + /** A display line for the clone step; `undefined` marks the step start. */ + onProgress: (detail?: string) => void; +}; + +export type SetupCommandsInput = { + commands: readonly string[]; + directory: string; + env: Record; + redact: (text: string) => string; + signal: AbortSignal; + /** Receives each command's start, redacted output and end. */ + onEvent: (event: ControlPlaneSetupEvent) => void; + /** Safe identifiers appended to the failure log line; never values. */ + logContext: string; +}; + +export type RepositoryPreparation = { + runNetworkGit: ( + args: string[], + options: ProcessOptions, + redact: (text: string) => string, + signal: AbortSignal + ) => Promise; + cloneRepository: (input: CloneInput) => Promise; + runSetupCommands: (input: SetupCommandsInput) => Promise; + headCommit: (directory: string, env: Record) => Promise; +}; + +export function createRepositoryPreparation( + deps: RepositoryPreparationDeps +): RepositoryPreparation { + const { runGit, runSetup, sleep, mkdir, hasGit, log } = deps; + + /** Runs a git command that talks to the remote; a network failure retries with backoff. */ + async function runNetworkGit( + args: string[], + options: ProcessOptions, + redact: (text: string) => string, + signal: AbortSignal + ): Promise { + let last: ExecResult | undefined; + for (let attempt = 1; attempt <= CLONE_RETRY_ATTEMPTS; attempt += 1) { + signal.throwIfAborted(); + last = await runGit(args, options); + if (last.exitCode === 0) return last; + const failure = gitOperationError(last, 'clone', redact); + if (!isNetworkFailure(failure.subtype) || attempt === CLONE_RETRY_ATTEMPTS) return last; + await sleep(CLONE_RETRY_BACKOFF_MS[attempt - 1] ?? 0, signal); + } + return last as ExecResult; + } + + async function cloneRepository(input: CloneInput): Promise { + const { directory, env, redact, signal, onProgress } = input; + const spec = input; + onProgress(); + await mkdir(directory); + if (!spec.git || (await hasGit(directory))) return; + // Diagnostic route class only: a managed credential was injected into the + // clone URL, otherwise the plain (direct) URL is used. Never log the URL. + const gitRoute: GitRouteClass = spec.git.token ? 'managed' : 'direct'; + if (!(await hasGit(directory))) { + const cloneUrl = authenticatedGitUrl(spec.git.url, spec.git.token, spec.git.platform); + const gitConfigArgs = managedGitHubPreparationGitArgs(spec, 'clone'); + const useBlobless = isBloblessReviewCloneEligible( + spec.createdOnPlatform, + spec.git.platform, + spec.git.token + ); + const noCheckout = + spec.createdOnPlatform === 'code-review' && + spec.branch !== undefined && + isSyntheticReviewRef(spec.branch) && + spec.branchMode !== 'working'; + const startedAt = Date.now(); + let cloneAttempts = 0; + let filterRejected = false; + const runClone = async (): Promise => { + signal.throwIfAborted(); + cloneAttempts += 1; + const cloned = await runGit( + [ + ...gitConfigArgs, + 'clone', + '--progress', + ...(useBlobless && !filterRejected ? ['--filter=blob:none'] : []), + ...(noCheckout ? ['--no-checkout'] : []), + cloneUrl, + directory, + ], + { + env, + inheritEnv: false, + signal, + onOutput: createGitProgressReporter(progressText => + onProgress(`Cloning repository... ${progressText}`) + ), + } + ); + signal.throwIfAborted(); + return cloned; + }; + let lastError: WrapperBootstrapError | undefined; + for (let attempt = 1; attempt <= CLONE_RETRY_ATTEMPTS; attempt += 1) { + signal.throwIfAborted(); + if (attempt > 1) { + onProgress(`Retrying clone (attempt ${attempt} of ${CLONE_RETRY_ATTEMPTS})`); + } + let cloned = await runClone(); + if ( + useBlobless && + !filterRejected && + cloned.exitCode !== 0 && + !cloned.terminationReason && + /(?:filter(?:ing)?[^\r\n]*(?:not supported|not recognized|rejected)|(?:does not support|unsupported|invalid)[^\r\n]*filter)/i.test( + `${cloned.stderr}\n${cloned.stdout}` + ) + ) { + filterRejected = true; + log( + `control-plane blobless clone rejected; retrying full clone session=${spec.sessionId}` + ); + cloned = await runClone(); + } + if (cloned.exitCode === 0) { + lastError = undefined; + break; + } + lastError = gitOperationError(cloned, 'clone', redact, gitRoute); + if (!isNetworkFailure(lastError.subtype) || attempt === CLONE_RETRY_ATTEMPTS) break; + await sleep(CLONE_RETRY_BACKOFF_MS[attempt - 1] ?? 0, signal); + } + if (lastError) throw lastError; + let filterHonored = false; + if (useBlobless && !filterRejected) { + const promisor = await runGit(['config', '--get', 'remote.origin.promisor'], { + cwd: directory, + env, + inheritEnv: false, + signal, + }); + filterHonored = promisor.exitCode === 0 && promisor.stdout.trim() === 'true'; + } + const mode = filterRejected ? 'blobless_fallback' : filterHonored ? 'blobless' : 'full'; + log( + `control-plane clone complete session=${spec.sessionId} mode=${mode} attempts=${cloneAttempts} durationMs=${Date.now() - startedAt}` + ); + } + } + + async function runSetupCommands(input: SetupCommandsInput): Promise { + const { commands, directory, env, redact, signal, onEvent, logContext } = input; + // Spec §9: a setup failure must show the command output, so each command's + // start, output and end are reported, not only written to the wrapper log. + for (const [index, command] of commands.entries()) { + signal.throwIfAborted(); + const commandNumber = index + 1; + onEvent({ + type: CONTROL_PLANE_SETUP_EVENTS.started, + properties: { command: commandNumber, commandCount: commands.length }, + }); + const output = createOutputRedactor( + text => redact(cleanTerminalOutput(text)), + text => { + if (signal.aborted) return; + const cleaned = text.trim(); + if (!cleaned) return; + log(`control-plane setup command ${commandNumber} produced output`); + onEvent({ + type: CONTROL_PLANE_SETUP_EVENTS.output, + properties: { + command: commandNumber, + output: `${cleaned.slice(0, SETUP_OUTPUT_EVENT_LIMIT - 1)}\n`, + }, + }); + } + ); + const startedAt = Date.now(); + const result = await runSetup(command, directory, env, output.onOutput, signal); + output.flush(); + if (result.exitCode !== 0) { + const timedOut = result.terminationReason !== undefined; + log( + `control-plane setup command failed ${logContext} index=${index + 1} count=${commands.length} exitCode=${result.exitCode} terminationReason=${result.terminationReason ?? 'nonzero'} elapsedMs=${Date.now() - startedAt} inactivityTimeoutMs=${SETUP_COMMAND_INACTIVITY_TIMEOUT_MS} hardTimeoutMs=${SETUP_COMMAND_HARD_TIMEOUT_MS}` + ); + const message = `Setup command ${commandNumber} ${timedOut ? 'timed out' : 'failed'}`; + onEvent({ + type: CONTROL_PLANE_SETUP_EVENTS.finished, + properties: { command: commandNumber, exitCode: result.exitCode, safeError: message }, + }); + throw new WrapperBootstrapError({ + code: 'WORKSPACE_SETUP_FAILED', + subtype: timedOut ? 'setup_command_timeout' : 'setup_command_failed', + message, + retryable: true, + }); + } + onEvent({ + type: CONTROL_PLANE_SETUP_EVENTS.finished, + properties: { command: commandNumber, exitCode: 0 }, + }); + } + } + + async function headCommit(directory: string, env: Record): Promise { + const result = await runGit(['rev-parse', 'HEAD'], { + cwd: directory, + env, + inheritEnv: false, + }); + return result.exitCode === 0 ? result.stdout.trim() : ''; + } + + return { runNetworkGit, cloneRepository, runSetupCommands, headCommit }; +} diff --git a/services/cloud-agent-next/wrapper/src/control-plane/workspace-adoption.test.ts b/services/cloud-agent-next/wrapper/src/control-plane/workspace-adoption.test.ts index e89257a002a..3759e794ce1 100644 --- a/services/cloud-agent-next/wrapper/src/control-plane/workspace-adoption.test.ts +++ b/services/cloud-agent-next/wrapper/src/control-plane/workspace-adoption.test.ts @@ -5,7 +5,6 @@ import os from 'node:os'; import path from 'node:path'; import { emptyWorkspaceDirectory, - removeStaleHomes, stripGitCredentials, truncateWrapperLog, } from './workspace-adoption.js'; @@ -135,24 +134,6 @@ describe('emptyWorkspaceDirectory', () => { }); }); -describe('removeStaleHomes', () => { - it("removes every home but this route's own", async () => { - const homes = path.join(root, 'homes'); - await fs.mkdir(path.join(homes, 'mine', '.cache'), { recursive: true }); - await fs.mkdir(path.join(homes, 'previous', '.npm'), { recursive: true }); - await fs.writeFile(path.join(homes, 'previous', '.npmrc'), 'token'); - await fs.writeFile(path.join(homes, 'stray-file'), 'x'); - - await removeStaleHomes(homes, path.join(homes, 'mine')); - - expect(await fs.readdir(homes)).toEqual(['mine']); - }); - - it('is a no-op when there is no home root', async () => { - expect(await rejection(removeStaleHomes(path.join(root, 'absent'), 'x'))).toBeUndefined(); - }); -}); - describe('truncateWrapperLog', () => { it('empties the log in place so later appends continue in the same file', async () => { const log = path.join(root, 'wrapper.log'); diff --git a/services/cloud-agent-next/wrapper/src/control-plane/workspace-adoption.ts b/services/cloud-agent-next/wrapper/src/control-plane/workspace-adoption.ts index 831ba578518..2b79e954db0 100644 --- a/services/cloud-agent-next/wrapper/src/control-plane/workspace-adoption.ts +++ b/services/cloud-agent-next/wrapper/src/control-plane/workspace-adoption.ts @@ -24,8 +24,8 @@ function failure( * 1. point `origin` at this route's credential; * 2. fetch, pruning refs the remote dropped, and refresh the remote default; * 3. detach at the remote default branch, so a new branch starts from its tip; - * 4. drop every local branch. The captured session's branch, or this session's own - * from an earlier capture, would otherwise shadow a newer `origin/`. + * 4. drop every local branch. The builder's default branch would otherwise shadow + * a newer `origin/`. * * Any failure throws and the caller falls back to a clone. */ @@ -54,10 +54,10 @@ export async function refreshAdoptedRepository(input: { } /** - * Leave no credential in `.git` before a snapshot: `origin` is set to the bare URL - * and the reflogs and `FETCH_HEAD`, which can record the URL a command ran with, - * are cleared. Returns false when any of that fails, in which case the workspace - * must not be captured. + * Leave no credential in `.git` before an environment build captures it: + * `origin` is set to the bare URL and the reflogs and `FETCH_HEAD`, which can + * record the URL a command ran with, are cleared. Returns false when any of that + * fails, in which case the workspace must not be captured. */ export async function stripGitCredentials(input: { git: WorkspaceGit; @@ -78,23 +78,7 @@ export async function stripGitCredentials(input: { } /** - * A restored container still holds the session homes of the allocation that took - * the snapshot. Setup re-runs and recreates what it needs under this route's own - * home, so every other entry is stale and only risks being read as current state - * (the log uploader archives each one). - */ -export async function removeStaleHomes(homeRoot: string, keep: string): Promise { - const entries = await fs.readdir(homeRoot).catch(() => [] as string[]); - const keepName = path.basename(keep); - await Promise.all( - entries - .filter(name => name !== keepName) - .map(name => fs.rm(path.join(homeRoot, name), { recursive: true, force: true })) - ); -} - -/** - * Empty the wrapper log so a snapshot does not carry this allocation's lines into a + * Empty a log so a snapshot does not carry this allocation's lines into a * restored container, where they would be uploaded with the next session's logs. * The log is appended to per line, so truncating in place is safe. */ diff --git a/services/cloud-agent-next/wrapper/src/control-plane/workspace-capture.test.ts b/services/cloud-agent-next/wrapper/src/control-plane/workspace-capture.test.ts deleted file mode 100644 index 07477c74d40..00000000000 --- a/services/cloud-agent-next/wrapper/src/control-plane/workspace-capture.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, expect, it } from 'bun:test'; -import type { ControlPlaneWrapperFrame } from '../../../src/shared/control-plane-protocol.js'; -import { createWorkspaceCapture } from './workspace-capture.js'; - -function setup() { - const sent: ControlPlaneWrapperFrame[] = []; - const capture = createWorkspaceCapture({ send: frame => sent.push(frame) }); - return { sent, capture }; -} - -describe('createWorkspaceCapture', () => { - it('sends the request and resolves with the answer for that session', async () => { - const { sent, capture } = setup(); - - const pending = capture.request('ses_a', 'abc123', 1_000); - capture.onCaptured('ses_other', true); - capture.onCaptured('ses_a', true); - - expect(await pending).toBe(true); - expect(sent).toEqual([{ type: 'workspace.capture', sessionId: 'ses_a', commit: 'abc123' }]); - }); - - it('reports a capture the DO could not save as false', async () => { - const { capture } = setup(); - const pending = capture.request('ses_a', undefined, 1_000); - capture.onCaptured('ses_a', false); - expect(await pending).toBe(false); - }); - - it('omits an empty commit from the frame', async () => { - const { sent, capture } = setup(); - const pending = capture.request('ses_a', '', 1_000); - capture.onCaptured('ses_a', true); - await pending; - expect(sent).toEqual([{ type: 'workspace.capture', sessionId: 'ses_a' }]); - }); - - it('gives up at the backstop when no answer arrives', async () => { - const { capture } = setup(); - expect(await capture.request('ses_a', undefined, 20)).toBe(false); - // A late answer after the backstop has nothing to settle. - capture.onCaptured('ses_a', true); - }); - - it('settles a request still waiting when the session asks again', async () => { - const { capture } = setup(); - const first = capture.request('ses_a', undefined, 1_000); - const second = capture.request('ses_a', undefined, 1_000); - expect(await first).toBe(false); - capture.onCaptured('ses_a', true); - expect(await second).toBe(true); - }); -}); diff --git a/services/cloud-agent-next/wrapper/src/control-plane/workspace-capture.ts b/services/cloud-agent-next/wrapper/src/control-plane/workspace-capture.ts deleted file mode 100644 index 26c8926f85c..00000000000 --- a/services/cloud-agent-next/wrapper/src/control-plane/workspace-capture.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { ControlPlaneWrapperFrame } from '../../../src/shared/control-plane-protocol.js'; - -/** - * The wrapper's side of a repository capture: ask the Sandbox DO to snapshot the - * container, then wait for its answer. The wait is a backstop over the DO's own - * capture timeout, so a lost frame or a reconnect never holds preparation open. - */ -export type WorkspaceCapture = { - /** Resolves `true` only when the DO saved the snapshot; never rejects. */ - request(sessionId: string, commit: string | undefined, timeoutMs: number): Promise; - /** Feeds a `workspace.captured` frame to the request waiting for it. */ - onCaptured(sessionId: string, ok: boolean): void; -}; - -type Pending = { settle: (ok: boolean) => void }; - -export function createWorkspaceCapture(options: { - send: (frame: ControlPlaneWrapperFrame) => void; -}): WorkspaceCapture { - const pending = new Map(); - - return { - request(sessionId, commit, timeoutMs) { - // A new request for the session supersedes one still waiting. - pending.get(sessionId)?.settle(false); - return new Promise(resolve => { - const entry: Pending = { - settle: ok => { - clearTimeout(timer); - if (pending.get(sessionId) === entry) pending.delete(sessionId); - resolve(ok); - }, - }; - const timer = setTimeout(() => entry.settle(false), timeoutMs); - pending.set(sessionId, entry); - options.send({ - type: 'workspace.capture', - sessionId, - ...(commit === undefined || commit.length === 0 ? {} : { commit }), - }); - }); - }, - onCaptured(sessionId, ok) { - pending.get(sessionId)?.settle(ok); - }, - }; -} diff --git a/services/cloud-agent-next/wrapper/src/control-plane/workspace-home.ts b/services/cloud-agent-next/wrapper/src/control-plane/workspace-home.ts new file mode 100644 index 00000000000..5e4ce81bb5d --- /dev/null +++ b/services/cloud-agent-next/wrapper/src/control-plane/workspace-home.ts @@ -0,0 +1,50 @@ +import { createHash } from 'node:crypto'; +import os from 'node:os'; +import path from 'node:path'; +import type { ControlPlaneRouteSpec } from '../../../src/shared/control-plane-protocol.js'; +import { + CONTROL_HOME_ROOT, + LEGACY_CONTROL_HOME_DIR_NAME, +} from '../../../src/shared/runtime-environment.js'; + +/** Where a wrapper keeps HOME directories; tests point both roots at a temporary tree. */ +export type HomeRoots = { + /** Directory-keyed homes of `worktree` runtime allocations. */ + directory: string; + /** Per-session homes of allocations that keep their original layout. */ + legacy: string; +}; + +export function defaultHomeRoots(): HomeRoots { + return { + directory: CONTROL_HOME_ROOT, + legacy: path.join(os.tmpdir(), LEGACY_CONTROL_HOME_DIR_NAME), + }; +} + +/** + * The fixed HOME of one workspace directory. The environment builder and each + * session in that directory compute the same path, so `~/.cache` and other + * toolchains written by builder setup are the session's own home after restore. + */ +export function directoryHome(directory: string, root: string = CONTROL_HOME_ROOT): string { + return path.join(root, createHash('sha256').update(directory).digest('hex')); +} + +/** + * HOME for one route. A spec that still carries `runtimeIsolation` belongs to an + * allocation that keeps its per-session runtime and HOME; the Sandbox DO strips + * the field only for a negotiated `worktree` runtime, which keys HOME by + * directory alone. + */ +export function routeHome(spec: ControlPlaneRouteSpec, roots: HomeRoots): string { + if (spec.runtimeIsolation === 'per-session') { + const id = createHash('sha256') + .update(spec.sessionId) + .update('\0') + .update(spec.directory) + .digest('hex'); + return path.join(roots.legacy, id); + } + return directoryHome(spec.directory, roots.directory); +} diff --git a/services/cloud-agent-next/wrapper/src/control-plane/workspace-stamp.test.ts b/services/cloud-agent-next/wrapper/src/control-plane/workspace-stamp.test.ts index 361f1430ce4..a33749d2fcb 100644 --- a/services/cloud-agent-next/wrapper/src/control-plane/workspace-stamp.test.ts +++ b/services/cloud-agent-next/wrapper/src/control-plane/workspace-stamp.test.ts @@ -4,15 +4,12 @@ import os from 'node:os'; import path from 'node:path'; import { classifyWorkspace, - planStamp, readWorkspaceStamp, - snapshotAction, - SNAPSHOT_MAX_GENERATION, - SNAPSHOT_REFRESH_AFTER_MS, workspaceStampPath, writeWorkspaceStamp, } from './workspace-stamp.js'; +// Lineage that wrappers which captured session snapshots wrote; still readable. const LINEAGE = { capturedAt: 1_000, generation: 0 }; let root: string; @@ -36,6 +33,14 @@ describe('workspace stamp', () => { expect(workspaceStampPath('/workspace/app', false)).toBe('/workspace/app.kilo-workspace.json'); }); + it('round-trips the current shape without lineage', async () => { + await writeWorkspaceStamp(directory, true, { allocationId: 'builder', commit: 'c1' }); + expect(await readWorkspaceStamp(directory, true)).toEqual({ + allocationId: 'builder', + commit: 'c1', + }); + }); + it('round-trips and replaces without leaving a temporary file', async () => { await writeWorkspaceStamp(directory, true, { ...LINEAGE, allocationId: 'a', commit: 'c1' }); await writeWorkspaceStamp(directory, true, { ...LINEAGE, allocationId: 'b', commit: 'c2' }); @@ -58,7 +63,7 @@ describe('workspace stamp', () => { JSON.stringify({ ...LINEAGE, allocationId: 'a', commit: 'c', extra: 1 }) ); expect(await readWorkspaceStamp(directory, true)).toBeNull(); - await fs.writeFile(file, JSON.stringify({ allocationId: 'a', commit: 'c' })); + await fs.writeFile(file, JSON.stringify({ allocationId: 'a' })); expect(await readWorkspaceStamp(directory, true)).toBeNull(); await fs.writeFile(file, 'ready\n'); expect(await readWorkspaceStamp(directory, true)).toBeNull(); @@ -76,54 +81,3 @@ describe('workspace stamp', () => { expect(classifyWorkspace({ ...input, hasGit: false, stamp: other })).toBe('empty'); }); }); - -describe('planning a stamp', () => { - const now = 10 * SNAPSHOT_REFRESH_AFTER_MS; - const previous = (age: number, generation: number) => ({ - allocationId: 'before', - commit: 'c', - capturedAt: now - age, - generation, - }); - - it('captures a fresh clone as generation 0 and nothing else a clone did not make', () => { - expect(planStamp({ cloning: true, adopted: false, previous: null, now })).toEqual({ - lineage: { capturedAt: now, generation: 0 }, - capture: true, - }); - expect(planStamp({ cloning: false, adopted: false, previous: null, now }).capture).toBe(false); - }); - - it('refreshes an adopted snapshot once it is old enough', () => { - const plan = planStamp({ - cloning: false, - adopted: true, - previous: previous(SNAPSHOT_REFRESH_AFTER_MS, 1), - now, - }); - expect(plan).toEqual({ lineage: { capturedAt: now, generation: 2 }, capture: true }); - }); - - it('keeps the capture time and generation of a snapshot that is not due', () => { - const plan = planStamp({ - cloning: false, - adopted: true, - previous: previous(SNAPSHOT_REFRESH_AFTER_MS - 1, 1), - now, - }); - expect(plan).toEqual({ - lineage: { capturedAt: now - SNAPSHOT_REFRESH_AFTER_MS + 1, generation: 1 }, - capture: false, - }); - }); - - it('rebuilds instead of refreshing once a due snapshot is at the generation cap', () => { - const due = (generation: number) => - snapshotAction(previous(SNAPSHOT_REFRESH_AFTER_MS, generation), now); - expect(due(SNAPSHOT_MAX_GENERATION - 1)).toBe('refresh'); - expect(due(SNAPSHOT_MAX_GENERATION)).toBe('rebuild'); - expect( - snapshotAction(previous(SNAPSHOT_REFRESH_AFTER_MS - 1, SNAPSHOT_MAX_GENERATION), now) - ).toBe('keep'); - }); -}); diff --git a/services/cloud-agent-next/wrapper/src/control-plane/workspace-stamp.ts b/services/cloud-agent-next/wrapper/src/control-plane/workspace-stamp.ts index 44575499f47..b7b983a5919 100644 --- a/services/cloud-agent-next/wrapper/src/control-plane/workspace-stamp.ts +++ b/services/cloud-agent-next/wrapper/src/control-plane/workspace-stamp.ts @@ -8,27 +8,18 @@ const workspaceStampSchema = z .object({ allocationId: z.string().min(1), commit: z.string(), - /** Epoch ms of the capture this workspace descends from (or would be captured at). */ - capturedAt: z.number().int().nonnegative(), - /** Refreshes since the last clone: 0 for a clone, one more per refresh of an adopted snapshot. */ - generation: z.number().int().nonnegative(), + // Lineage written by wrappers that captured session snapshots. Read only so + // a stamp in an older snapshot still classifies; never written again. + capturedAt: z.number().int().nonnegative().optional(), + generation: z.number().int().nonnegative().optional(), }) .strict(); -/** An adopted snapshot older than this is captured again, from the adopted workspace. */ -export const SNAPSHOT_REFRESH_AFTER_MS = 4 * 24 * 60 * 60 * 1000; /** - * Refreshes allowed on top of a clone. Each capture stacks on the last, so deleted - * files and untracked leftovers accumulate; once the cap is reached and the - * snapshot is due again, it is rebuilt from a fresh clone instead. - */ -export const SNAPSHOT_MAX_GENERATION = 4; - -/** - * Written once a workspace is fully prepared: which allocation prepared it, at - * which commit, and when and how many refreshes ago it was cloned and captured. A - * stamp from another allocation means the files came from a repository snapshot, - * so they are adopted (reconciled) before use. + * Written once a workspace is fully prepared: which allocation prepared it and at + * which commit. A stamp from another allocation means the files came from an + * environment snapshot (the builder stamps with its own allocation), so they are + * adopted (reconciled) before use. */ export type WorkspaceStamp = z.infer; @@ -89,47 +80,3 @@ export function classifyWorkspace(input: { // A stamp beside a directory that has no repository has nothing to adopt. return hasGit ? 'foreign' : 'empty'; } - -/** What an adopted snapshot of this age and generation needs, if anything. */ -export type SnapshotAction = - /** Not due yet: adopt it as it is. */ - | 'keep' - /** Due: adopt it, then capture the adopted workspace as the next generation. */ - | 'refresh' - /** Due at the generation cap: discard it, clone fresh and capture as generation 0. */ - | 'rebuild'; - -export function snapshotAction(previous: WorkspaceStamp, now: number): SnapshotAction { - if (now - previous.capturedAt < SNAPSHOT_REFRESH_AFTER_MS) return 'keep'; - return previous.generation < SNAPSHOT_MAX_GENERATION ? 'refresh' : 'rebuild'; -} - -export type StampPlan = { - lineage: Pick; - /** Whether this preparation should save the workspace as a repository snapshot. */ - capture: boolean; -}; - -/** - * What a prepared workspace carries into its stamp and whether it is captured. - * A fresh clone is captured as generation 0. An adopted snapshot is captured again - * only when it is due for a refresh, and otherwise keeps its capture time and - * generation, so its age keeps counting from the snapshot taken. - */ -export function planStamp(input: { - cloning: boolean; - adopted: boolean; - previous: WorkspaceStamp | null; - now: number; -}): StampPlan { - const { cloning, adopted, previous, now } = input; - if (adopted && previous !== null) { - return snapshotAction(previous, now) === 'refresh' - ? { lineage: { capturedAt: now, generation: previous.generation + 1 }, capture: true } - : { - lineage: { capturedAt: previous.capturedAt, generation: previous.generation }, - capture: false, - }; - } - return { lineage: { capturedAt: now, generation: 0 }, capture: cloning }; -} diff --git a/services/cloud-agent-next/wrapper/src/control/control-upload-episode.test.ts b/services/cloud-agent-next/wrapper/src/control/control-upload-episode.test.ts index ffe04459a29..6e0eabe2cc7 100644 --- a/services/cloud-agent-next/wrapper/src/control/control-upload-episode.test.ts +++ b/services/cloud-agent-next/wrapper/src/control/control-upload-episode.test.ts @@ -48,7 +48,7 @@ describe('native upload episode projection', () => { uploadUrl: UPLOAD_URL, uploadGrant: UPLOAD_GRANT, wrapperLogPath, - homeRoot: dir, + homeRoots: [dir], fetch: () => Promise.resolve(new Response(null, { status: archiveStatuses.shift() ?? 204 })), onDiagnostic: diagnostics.onDiagnostic, diff --git a/services/cloud-agent-next/wrapper/src/control/file-log-uploader.ts b/services/cloud-agent-next/wrapper/src/control/file-log-uploader.ts index 6f910f37e56..23cd1080f84 100644 --- a/services/cloud-agent-next/wrapper/src/control/file-log-uploader.ts +++ b/services/cloud-agent-next/wrapper/src/control/file-log-uploader.ts @@ -1,5 +1,4 @@ import { existsSync, readdirSync, statSync } from 'node:fs'; -import os from 'node:os'; import path from 'node:path'; import { CONTROL_LOG_ARCHIVE_NAME, @@ -12,6 +11,7 @@ import { } from '../../../src/shared/control-diagnostics.js'; import { createTarStream, type TarArchiveEntry } from '../log-uploader.js'; import { logToFile, withTimeoutAndAbort } from '../utils.js'; +import { defaultHomeRoots } from '../control-plane/workspace-home.js'; export type ControlFileLogUploader = { start: (intervalMs?: number) => void; @@ -24,7 +24,8 @@ type Options = { uploadUrl?: string; uploadGrant?: string; wrapperLogPath?: string; - homeRoot?: string; + /** Roots whose `/.local/share/kilo/log` directories are archived. */ + homeRoots?: readonly string[]; fetch?: (url: string, init: RequestInit) => Promise; onDiagnostic?: ControlDiagnosticReporter; projector?: ControlDiagnosticProjector; @@ -38,17 +39,27 @@ const UPLOAD_TIMEOUT_MS = 15_000; const FINAL_UPLOAD_TIMEOUT_MS = 5_000; const INTERVAL_MS = 30_000; -export function defaultControlWorktreeHomeRoot(): string { - return path.join(os.tmpdir(), 'kilo-worktrees'); +/** + * The directory-keyed home root and the per-session root that allocations + * created before it still use. Both are read while such allocations exist. + */ +export function defaultControlHomeRoots(): string[] { + const roots = defaultHomeRoots(); + return [roots.legacy, roots.directory]; } -export function listControlKiloLogDirs(homeRoot = defaultControlWorktreeHomeRoot()): string[] { - if (!existsSync(homeRoot)) return []; +/** Kilo log directories under each home of each root; nothing else is collected. */ +export function listControlKiloLogDirs( + homeRoots: readonly string[] = defaultControlHomeRoots() +): string[] { const dirs: string[] = []; - for (const entry of readdirSync(homeRoot, { withFileTypes: true })) { - if (!entry.isDirectory() || entry.isSymbolicLink()) continue; - const logDir = path.join(homeRoot, entry.name, KILO_LOG_RELATIVE); - if (existsSync(logDir)) dirs.push(logDir); + for (const homeRoot of homeRoots) { + if (!existsSync(homeRoot)) continue; + for (const entry of readdirSync(homeRoot, { withFileTypes: true })) { + if (!entry.isDirectory() || entry.isSymbolicLink()) continue; + const logDir = path.join(homeRoot, entry.name, KILO_LOG_RELATIVE); + if (existsSync(logDir)) dirs.push(logDir); + } } return dirs; } @@ -86,10 +97,14 @@ export function selectControlFileLogPaths(input: { return selected; } +/** + * Archive entries named relative to the home root that contains each file. Home + * names are hashes, so two roots never produce the same relative name. + */ function archiveEntries( files: string[], wrapperLogPath: string, - homeRoot: string + homeRoots: readonly string[] ): TarArchiveEntry[] { const entries: TarArchiveEntry[] = []; for (const filePath of files) { @@ -97,9 +112,12 @@ function archiveEntries( entries.push({ directory: path.dirname(filePath), name: path.basename(filePath) }); continue; } - const relative = path.relative(homeRoot, filePath); - if (relative.startsWith('..') || path.isAbsolute(relative)) continue; - entries.push({ directory: homeRoot, name: relative }); + for (const homeRoot of homeRoots) { + const relative = path.relative(homeRoot, filePath); + if (relative.startsWith('..') || path.isAbsolute(relative)) continue; + entries.push({ directory: homeRoot, name: relative }); + break; + } } return entries; } @@ -140,7 +158,7 @@ export function createControlFileLogUploader(options: Options): ControlFileLogUp type Upload = { promise: Promise; abort: AbortController }; const upload = options.fetch ?? fetch; const wrapperLogPath = options.wrapperLogPath ?? DEFAULT_WRAPPER_LOG_PATH; - const homeRoot = options.homeRoot ?? defaultControlWorktreeHomeRoot(); + const homeRoots = options.homeRoots ?? defaultControlHomeRoots(); let intervalId: ReturnType | undefined; let activeUpload: Upload | undefined; let queuedUpload: Upload | undefined; @@ -209,10 +227,10 @@ export function createControlFileLogUploader(options: Options): ControlFileLogUp const files = [ ...selectControlFileLogPaths({ wrapperLogPath, - kiloLogDirs: listControlKiloLogDirs(homeRoot), + kiloLogDirs: listControlKiloLogDirs(homeRoots), }), ]; - const entries = archiveEntries(files, wrapperLogPath, homeRoot); + const entries = archiveEntries(files, wrapperLogPath, homeRoots); if (entries.length === 0) return; const body = await collectArchive(entries, CONTROL_LOG_MAX_ARCHIVE_BYTES); abort.signal.throwIfAborted(); diff --git a/services/cloud-agent-next/wrapper/src/environment-builder/cleanup.ts b/services/cloud-agent-next/wrapper/src/environment-builder/cleanup.ts new file mode 100644 index 00000000000..dbce67055f9 --- /dev/null +++ b/services/cloud-agent-next/wrapper/src/environment-builder/cleanup.ts @@ -0,0 +1,121 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { + stripGitCredentials, + truncateWrapperLog, + type WorkspaceGit, +} from '../control-plane/workspace-adoption.js'; + +/** + * Pre-capture cleanup (plan "Build admission and execution", steps 6 and 7). + * Setup descendants are stopped so nothing rewrites a file while the platform + * snapshots the container, then platform-managed credential material is removed + * from the workspace and the fixed home. Caches and toolchains stay. A cleanup + * that cannot be completed or verified fails the build instead of capturing. + * + * Setup commands can still write a secret into an arbitrary file; known paths + * are all that cleanup can prove. Snapshot contents stay sensitive environment + * data (plan "Credentials and snapshot contents"). + */ + +/** Git credential files a setup or the platform may leave in the fixed home. */ +export const HOME_CREDENTIAL_FILES = [ + '.git-credentials', + path.join('.config', 'git', 'credentials'), +] as const; + +const STOP_GRACE_MS = 2_000; + +/** + * Stop every other process in the container. The builder is PID 1, so + * `kill(-1)` reaches every process except itself; the call is refused anywhere + * else, so a test or a misconfigured start cannot signal a host. + */ +export async function stopOtherProcesses( + deps: { + pid?: number; + kill?: (pid: number, signal: NodeJS.Signals) => void; + sleep?: (ms: number) => Promise; + } = {} +): Promise { + const pid = deps.pid ?? process.pid; + if (pid !== 1) throw new Error('Refusing to stop processes outside a builder container'); + const kill = deps.kill ?? ((target, signal) => process.kill(target, signal)); + const sleep = deps.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))); + const signalAll = (signal: NodeJS.Signals): void => { + try { + kill(-1, signal); + } catch (error) { + // ESRCH: no other process exists. + if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error; + } + }; + signalAll('SIGTERM'); + await sleep(STOP_GRACE_MS); + signalAll('SIGKILL'); +} + +export type CleanupInput = { + git: WorkspaceGit; + directory: string; + home: string; + /** The credential-free repository URL `origin` is left at. */ + bareUrl: string; + /** Build-scoped values that must not remain in Git metadata. */ + canaries: readonly string[]; + logPath: string | undefined; +}; + +/** Files whose text can carry a URL-bound credential after a clone and fetch. */ +function gitMetadataFiles(directory: string, home: string): string[] { + return [ + path.join(directory, '.git', 'config'), + path.join(directory, '.git', 'FETCH_HEAD'), + path.join(directory, '.git', 'packed-refs'), + path.join(home, '.gitconfig'), + path.join(home, '.config', 'git', 'config'), + ...HOME_CREDENTIAL_FILES.map(file => path.join(home, file)), + ]; +} + +async function containsCanary(file: string, canaries: readonly string[]): Promise { + let text: string; + try { + text = await fs.readFile(file, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false; + throw error; + } + return canaries.some(canary => canary.length > 0 && text.includes(canary)); +} + +/** + * Leave no credential in Git metadata, the known home credential files or the + * builder log, then verify it. Returns a safe failure phrase, or null when the + * workspace may be captured. + */ +export async function removeBuildCredentials(input: CleanupInput): Promise { + const bare = await stripGitCredentials({ + git: input.git, + directory: input.directory, + bareUrl: input.bareUrl, + }); + if (!bare) return 'repository credential could not be removed'; + try { + for (const file of HOME_CREDENTIAL_FILES) { + await fs.rm(path.join(input.home, file), { force: true }); + } + } catch { + return 'home credential files could not be removed'; + } + await truncateWrapperLog(input.logPath); + try { + for (const file of gitMetadataFiles(input.directory, input.home)) { + if (await containsCanary(file, input.canaries)) + return 'a credential remained in Git metadata'; + } + } catch { + return 'credential cleanup could not be verified'; + } + return null; +} diff --git a/services/cloud-agent-next/wrapper/src/environment-builder/client.ts b/services/cloud-agent-next/wrapper/src/environment-builder/client.ts new file mode 100644 index 00000000000..49240dbecd1 --- /dev/null +++ b/services/cloud-agent-next/wrapper/src/environment-builder/client.ts @@ -0,0 +1,90 @@ +import { + environmentBuildEventsReplySchema, + environmentBuildInputSchema, + type EnvironmentBuildEvents, + type EnvironmentBuildEventsReply, + type EnvironmentBuildInput, + type EnvironmentBuildResult, +} from '../../../src/shared/environment-build-protocol.js'; + +/** + * The builder's side of the build-scoped control endpoint. Every call is short + * and bounded; a transient failure retries with backoff, a refusal (the build + * was superseded, canceled or deleted, or the credential is not this build's) + * stops the builder instead of retrying. + */ + +const CALL_TIMEOUT_MS = 15_000; +const RETRY_BACKOFF_MS = [1_000, 2_000, 4_000, 8_000]; + +/** The coordinator refused this builder; it must stop work rather than retry. */ +export class BuilderRefusedError extends Error { + constructor(readonly status: number) { + super(`Environment build endpoint refused the builder (${status})`); + this.name = 'BuilderRefusedError'; + } +} + +export type BuilderClient = { + input(): Promise; + events(batch: EnvironmentBuildEvents): Promise; + result(result: EnvironmentBuildResult): Promise; +}; + +export type BuilderClientDeps = { + baseUrl: string; + credential: string; + fetch?: (url: string, init: RequestInit) => Promise; + sleep?: (ms: number) => Promise; + /** Attempts per call, including the first. */ + attempts?: number; +}; + +function retryable(status: number): boolean { + return status === 408 || status === 429 || status >= 500; +} + +export function createBuilderClient(deps: BuilderClientDeps): BuilderClient { + const send = deps.fetch ?? ((url, init) => fetch(url, init)); + const sleep = deps.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))); + const attempts = deps.attempts ?? RETRY_BACKOFF_MS.length + 1; + const base = deps.baseUrl.replace(/\/$/, ''); + + async function call(path: string, init: RequestInit): Promise { + let lastError: unknown; + for (let attempt = 1; attempt <= attempts; attempt += 1) { + try { + const response = await send(`${base}/${path}`, { + ...init, + headers: { + authorization: `Bearer ${deps.credential}`, + ...(init.body === undefined ? {} : { 'content-type': 'application/json' }), + }, + signal: AbortSignal.timeout(CALL_TIMEOUT_MS), + }); + if (response.ok) return response.status === 204 ? undefined : await response.json(); + if (!retryable(response.status)) throw new BuilderRefusedError(response.status); + lastError = new Error(`Environment build endpoint returned ${response.status}`); + } catch (error) { + if (error instanceof BuilderRefusedError) throw error; + lastError = error; + } + if (attempt < attempts) await sleep(RETRY_BACKOFF_MS[attempt - 1] ?? 8_000); + } + throw lastError instanceof Error ? lastError : new Error('Environment build endpoint failed'); + } + + return { + async input() { + return environmentBuildInputSchema.parse(await call('input', { method: 'GET' })); + }, + async events(batch) { + return environmentBuildEventsReplySchema.parse( + await call('events', { method: 'POST', body: JSON.stringify(batch) }) + ); + }, + async result(result) { + await call('result', { method: 'POST', body: JSON.stringify(result) }); + }, + }; +} diff --git a/services/cloud-agent-next/wrapper/src/environment-builder/main.ts b/services/cloud-agent-next/wrapper/src/environment-builder/main.ts new file mode 100644 index 00000000000..e7f060033ef --- /dev/null +++ b/services/cloud-agent-next/wrapper/src/environment-builder/main.ts @@ -0,0 +1,144 @@ +import fs from 'node:fs/promises'; +import { appendFileSync } from 'node:fs'; +import { + ENVIRONMENT_BUILD_LOG_PATH, + type EnvironmentBuildResult, +} from '../../../src/shared/environment-build-protocol.js'; +import { + SETUP_COMMAND_HARD_TIMEOUT_MS, + SETUP_COMMAND_INACTIVITY_TIMEOUT_MS, + createRepositoryPreparation, +} from '../control-plane/repository-preparation.js'; +import { writeWorkspaceStamp } from '../control-plane/workspace-stamp.js'; +import { git } from '../utils.js'; +import { runSetupScript } from '../setup-script.js'; +import { BuilderRefusedError, createBuilderClient } from './client.js'; +import { removeBuildCredentials, stopOtherProcesses } from './cleanup.js'; +import { createBuildReporter } from './reporter.js'; +import { runEnvironmentBuild } from './run.js'; +import { takeRunner } from './runner-file.js'; + +/** + * Environment builder entry point: the main process of a dedicated builder + * container. It runs no Kilo and holds no session state. After it reports its + * result it stays alive and idle, so the coordinator can snapshot the running + * container and then stop it. + */ + +const REPORT_INTERVAL_MS = 2_000; + +const logPath = ENVIRONMENT_BUILD_LOG_PATH; + +function log(message: string): void { + try { + appendFileSync(logPath, `${new Date().toISOString()} ${message}\n`); + } catch { + // Diagnostics only. + } +} + +function idleUntilStopped(): Promise { + // An unresolved promise alone does not keep the process alive. + setInterval(() => undefined, 1 << 30); + return new Promise(() => undefined); +} + +async function main(): Promise { + const runner = await takeRunner(); + if (runner === null) { + log('environment builder received no endpoint or credential'); + return idleUntilStopped(); + } + const client = createBuilderClient({ baseUrl: runner.url, credential: runner.credential }); + const inherited = { ...process.env }; + + let input; + try { + input = await client.input(); + } catch (error) { + log( + `environment builder input unavailable: ${error instanceof Error ? error.message : 'unknown'}` + ); + if (!(error instanceof BuilderRefusedError)) { + await client + .result({ outcome: 'failed', step: 'clone', reason: 'input_unavailable' }) + .catch(() => undefined); + } + return idleUntilStopped(); + } + + const controller = new AbortController(); + const deadline = setTimeout( + () => controller.abort(new Error('build deadline')), + Math.max(0, input.deadlineAt - Date.now()) + ); + const reporter = createBuildReporter({ + client, + intervalMs: REPORT_INTERVAL_MS, + onCancel: () => controller.abort(new Error('build canceled')), + log, + }); + reporter.start(); + const repository = createRepositoryPreparation({ + runGit: (args, options) => git(args, options), + runSetup: (command, directory, env, onOutput, signal) => + runSetupScript(command, { + cwd: directory, + env, + inheritEnv: false, + ...(signal ? { signal } : {}), + inactivityTimeoutMs: SETUP_COMMAND_INACTIVITY_TIMEOUT_MS, + hardTimeoutMs: SETUP_COMMAND_HARD_TIMEOUT_MS, + ...(onOutput ? { onOutput } : {}), + }), + sleep: (ms, signal) => + new Promise(resolve => { + const timer = setTimeout(resolve, ms); + signal.addEventListener('abort', () => { + clearTimeout(timer); + resolve(undefined); + }); + }), + mkdir: directory => fs.mkdir(directory, { recursive: true }).then(() => undefined), + hasGit: directory => + fs.access(`${directory}/.git/HEAD`).then( + () => true, + () => false + ), + log, + }); + + let result: EnvironmentBuildResult; + try { + result = await runEnvironmentBuild({ + input, + reporter, + repository, + runGit: (args, options) => git(args, options), + stopProcesses: () => stopOtherProcesses(), + cleanup: removeBuildCredentials, + writeStamp: (directory, stamp) => writeWorkspaceStamp(directory, true, stamp), + signal: controller.signal, + inherited, + logPath, + }); + } finally { + clearTimeout(deadline); + } + log(`environment builder finished outcome=${result.outcome}`); + reporter.stop(); + await reporter.flush(); + try { + await client.result(result); + } catch (error) { + log( + `environment builder result not delivered: ${error instanceof Error ? error.message : 'unknown'}` + ); + } + return idleUntilStopped(); +} + +void main().catch(error => { + log(`environment builder failed: ${error instanceof Error ? error.message : 'unknown'}`); + return idleUntilStopped(); +}); diff --git a/services/cloud-agent-next/wrapper/src/environment-builder/reporter.ts b/services/cloud-agent-next/wrapper/src/environment-builder/reporter.ts new file mode 100644 index 00000000000..88db89ff90a --- /dev/null +++ b/services/cloud-agent-next/wrapper/src/environment-builder/reporter.ts @@ -0,0 +1,125 @@ +import { + ENVIRONMENT_BUILD_LOG_BATCH_MAX_BYTES, + ENVIRONMENT_BUILD_LOG_BATCH_MAX_LINES, + ENVIRONMENT_BUILD_LOG_LINE_MAX_CHARS, + type EnvironmentBuildRunnerStep, +} from '../../../src/shared/environment-build-protocol.js'; +import type { BuilderClient } from './client.js'; + +/** + * Batches redacted build output and reports liveness. One flush per interval + * sends the current step, the buffered lines and an implicit heartbeat. A failed + * flush keeps its lines for the next one; the buffer is bounded and drops the + * oldest lines with one marker, so a slow endpoint cannot grow the builder's + * memory. A `cancel` reply aborts the build. + */ + +/** Unsent output retained while the endpoint is unreachable. */ +const MAX_PENDING_BYTES = 1024 * 1024; +const DROPPED_MARKER = '[earlier output dropped while the build endpoint was unreachable]'; + +export type BuildReporter = { + setStep(step: EnvironmentBuildRunnerStep): void; + line(text: string): void; + /** Send everything buffered now; resolves after the call settles. */ + flush(): Promise; + start(): void; + stop(): void; +}; + +export function createBuildReporter(deps: { + client: Pick; + intervalMs: number; + onCancel: () => void; + log: (message: string) => void; +}): BuildReporter { + let sequence = 0; + let step: EnvironmentBuildRunnerStep | undefined; + let stepSent = true; + let pending: string[] = []; + let pendingBytes = 0; + let dropped = false; + let timer: ReturnType | undefined; + let inFlight: Promise | undefined; + + function take(): string[] { + const batch: string[] = []; + let bytes = 0; + if (dropped) { + batch.push(DROPPED_MARKER); + dropped = false; + bytes = Buffer.byteLength(DROPPED_MARKER, 'utf8'); + } + while (pending.length > 0 && batch.length < ENVIRONMENT_BUILD_LOG_BATCH_MAX_LINES) { + const next = pending[0]; + const size = Buffer.byteLength(next, 'utf8'); + if (batch.length > 0 && bytes + size > ENVIRONMENT_BUILD_LOG_BATCH_MAX_BYTES) break; + batch.push(next); + bytes += size; + pending.shift(); + pendingBytes -= size; + } + return batch; + } + + async function send(): Promise { + const logs = take(); + const batchStep = stepSent ? undefined : step; + stepSent = true; + try { + const reply = await deps.client.events({ + sequence: sequence++, + ...(batchStep === undefined ? {} : { step: batchStep }), + ...(logs.length === 0 ? {} : { logs }), + }); + if (reply.action === 'cancel') deps.onCancel(); + } catch (error) { + // Retain what was not delivered; the next flush sends it again under a new + // sequence, which the coordinator applies once. + pending = [...logs.filter(line => line !== DROPPED_MARKER), ...pending]; + pendingBytes = pending.reduce((sum, line) => sum + Buffer.byteLength(line, 'utf8'), 0); + if (batchStep !== undefined) stepSent = false; + deps.log( + `environment builder events failed: ${error instanceof Error ? error.message : 'unknown'}` + ); + } + } + + function flush(): Promise { + const previous = inFlight ?? Promise.resolve(); + const next = previous.then(send); + inFlight = next.finally(() => { + if (inFlight === next) inFlight = undefined; + }); + return next; + } + + return { + setStep(next) { + step = next; + stepSent = false; + }, + line(text) { + const bounded = + text.length > ENVIRONMENT_BUILD_LOG_LINE_MAX_CHARS + ? `${text.slice(0, ENVIRONMENT_BUILD_LOG_LINE_MAX_CHARS - 1)}…` + : text; + pending.push(bounded); + pendingBytes += Buffer.byteLength(bounded, 'utf8'); + while (pendingBytes > MAX_PENDING_BYTES && pending.length > 1) { + const removed = pending.shift() as string; + pendingBytes -= Buffer.byteLength(removed, 'utf8'); + dropped = true; + } + }, + flush, + start() { + if (timer !== undefined) return; + timer = setInterval(() => void flush(), deps.intervalMs); + }, + stop() { + if (timer !== undefined) clearInterval(timer); + timer = undefined; + }, + }; +} diff --git a/services/cloud-agent-next/wrapper/src/environment-builder/run.test.ts b/services/cloud-agent-next/wrapper/src/environment-builder/run.test.ts new file mode 100644 index 00000000000..5f5951e4461 --- /dev/null +++ b/services/cloud-agent-next/wrapper/src/environment-builder/run.test.ts @@ -0,0 +1,350 @@ +import { + environmentBuildInputSchema, + environmentBuildEventsSchema, +} from '../../../src/shared/environment-build-protocol.js'; +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { CONTROL_PLANE_SETUP_EVENTS } from '../../../src/shared/control-plane-protocol.js'; +import type { + EnvironmentBuildEvents, + EnvironmentBuildInput, +} from '../../../src/shared/environment-build-protocol.js'; +import { createRepositoryPreparation } from '../control-plane/repository-preparation.js'; +import { directoryHome } from '../control-plane/workspace-home.js'; +import { readWorkspaceStamp, writeWorkspaceStamp } from '../control-plane/workspace-stamp.js'; +import { git, runProcess } from '../utils.js'; +import { removeBuildCredentials, stopOtherProcesses } from './cleanup.js'; +import { createBuildReporter } from './reporter.js'; +import { builderSetupEnvironment, runEnvironmentBuild, setupEventLine } from './run.js'; + +const GIT_ENV = { + ...process.env, + GIT_AUTHOR_NAME: 'Test', + GIT_AUTHOR_EMAIL: 'test@example.com', + GIT_COMMITTER_NAME: 'Test', + GIT_COMMITTER_EMAIL: 'test@example.com', + GIT_CONFIG_GLOBAL: '/dev/null', + GIT_CONFIG_SYSTEM: '/dev/null', +}; + +let root: string; +let origin: string; +let directory: string; +let homeRoot: string; +let tip: string; + +function gitIn(cwd: string, ...args: string[]): string { + return execFileSync('git', args, { cwd, env: GIT_ENV, encoding: 'utf8' }).trim(); +} + +beforeEach(async () => { + root = await fs.mkdtemp(path.join(os.tmpdir(), 'environment-builder-')); + origin = path.join(root, 'origin.git'); + const author = path.join(root, 'author'); + directory = path.join(root, 'workspace', 'app'); + homeRoot = path.join(root, 'home'); + execFileSync('git', ['init', '-q', '--bare', '-b', 'main', origin], { env: GIT_ENV }); + execFileSync('git', ['clone', '-q', origin, author], { env: GIT_ENV, stdio: 'ignore' }); + await fs.writeFile(path.join(author, 'README'), 'one'); + gitIn(author, 'add', 'README'); + gitIn(author, 'commit', '-q', '-m', 'first'); + gitIn(author, 'push', '-q', 'origin', 'HEAD:main'); + tip = gitIn(author, 'rev-parse', 'HEAD'); +}); + +afterEach(async () => { + await fs.rm(root, { recursive: true, force: true }); +}); + +function buildInput(overrides: Partial = {}): EnvironmentBuildInput { + return { + environmentId: '11111111-1111-4111-8111-111111111111', + buildId: '22222222-2222-4222-8222-222222222222', + allocationId: '33333333-3333-4333-8333-333333333333', + git: { url: pathToFileURL(origin).toString() }, + directory: '/workspace/app', + commands: [], + env: { PLAIN: 'plain-value', API_TOKEN: 'secret-canary-value' }, + secretKeys: ['API_TOKEN'], + deadlineAt: Date.now() + 60_000, + ...overrides, + }; +} + +function harness(options: { cleanup?: typeof removeBuildCredentials } = {}) { + const lines: string[] = []; + const steps: string[] = []; + let stopped = 0; + const repository = createRepositoryPreparation({ + runGit: (args, opts) => git(args, opts), + runSetup: (command, dir, env, onOutput, signal) => + runProcess('sh', ['-c', command], { + cwd: dir, + env, + inheritEnv: false, + ...(signal ? { signal } : {}), + ...(onOutput ? { onOutput } : {}), + }), + sleep: async () => undefined, + mkdir: dir => fs.mkdir(dir, { recursive: true }).then(() => undefined), + hasGit: dir => + fs.access(path.join(dir, '.git', 'HEAD')).then( + () => true, + () => false + ), + log: () => undefined, + }); + const run = (input: EnvironmentBuildInput, signal = new AbortController().signal) => + runEnvironmentBuild({ + // The workspace path is fixed in the protocol; tests run it under a temp root. + input: { ...input, directory } as EnvironmentBuildInput, + reporter: { setStep: step => steps.push(step), line: line => lines.push(line) }, + repository, + runGit: (args, opts) => git(args, opts), + stopProcesses: async () => { + stopped += 1; + }, + cleanup: options.cleanup ?? removeBuildCredentials, + writeStamp: (dir, stamp) => writeWorkspaceStamp(dir, true, stamp), + signal, + inherited: { PATH: process.env.PATH, ENVIRONMENT_BUILD_CREDENTIAL: 'jwt', HOME: '/root' }, + logPath: undefined, + homeRoot, + }); + return { run, lines, steps, stopped: () => stopped }; +} + +describe('environment build run', () => { + it('accepts and executes a full 16,000-character environment script', async () => { + const commands = [ + ('#' + 'x'.repeat(15900) + '\nprintf prepared > long-script-proof').padEnd(16000, ' '), + ]; + const input = environmentBuildInputSchema.parse(buildInput({ commands })); + expect( + environmentBuildInputSchema.safeParse({ ...input, commands: [commands[0] + 'x'] }).success + ).toBe(false); + expect(await harness().run(input)).toMatchObject({ outcome: 'ready' }); + expect(await fs.readFile(path.join(directory, 'long-script-proof'), 'utf8')).toBe('prepared'); + }); + + it('clones the default branch, pins its commit, runs setup in the fixed home and stamps', async () => { + const h = harness(); + const result = await h.run( + buildInput({ + commands: [ + 'mkdir -p "$XDG_CACHE_HOME/tool" && echo "$PLAIN" > "$XDG_CACHE_HOME/tool/built"', + 'echo "token is $API_TOKEN"', + ], + }) + ); + + expect(result).toEqual({ + outcome: 'ready', + commit: tip, + branch: 'main', + setupDurationMs: expect.any(Number), + }); + const home = directoryHome(directory, homeRoot); + expect(await fs.readFile(path.join(home, '.cache', 'tool', 'built'), 'utf8')).toBe( + 'plain-value\n' + ); + expect(await readWorkspaceStamp(directory, true)).toEqual({ + allocationId: '33333333-3333-4333-8333-333333333333', + commit: tip, + }); + expect(h.steps).toEqual(['clone', 'setup', 'cleanup']); + expect(h.stopped()).toBe(1); + expect(h.lines.join('\n')).not.toContain('secret-canary-value'); + expect(h.lines).toContain('token is [REDACTED]'); + expect(h.lines).toContain(`Default branch main at ${tip}`); + }); + + it('removes the repository credential before it reports ready', async () => { + const h = harness(); + const token = 'gho_buildtokencanary1234567890'; + const url = pathToFileURL(origin).toString(); + // A file URL ignores credentials, so the token only appears where cleanup must remove it. + const result = await h.run( + buildInput({ git: { url, token }, commands: [`git config credential.helper store`] }) + ); + + expect(result.outcome).toBe('ready'); + expect(gitIn(directory, 'remote', 'get-url', 'origin')).toBe(url); + expect(await fs.readFile(path.join(directory, '.git', 'config'), 'utf8')).not.toContain(token); + expect(gitIn(directory, 'reflog', '--all')).toBe(''); + }); + + it('fails cleanup instead of reporting ready when a credential remains', async () => { + const h = harness({ cleanup: async () => 'a credential remained in Git metadata' }); + + expect(await h.run(buildInput())).toEqual({ + outcome: 'failed', + step: 'cleanup', + reason: 'cleanup_failed', + detail: 'a credential remained in Git metadata', + }); + expect(await readWorkspaceStamp(directory, true)).toBeNull(); + }); + + it('reports a failing or timed-out setup command with its step', async () => { + const h = harness(); + expect(await h.run(buildInput({ commands: ['exit 3'] }))).toEqual({ + outcome: 'failed', + step: 'setup', + reason: 'setup_failed', + }); + expect(h.lines.some(line => line.includes('Setup command 1 failed (exit 3)'))).toBe(true); + }); + + it('reports a clone failure for an unreachable repository', async () => { + const h = harness(); + expect( + await h.run( + buildInput({ git: { url: pathToFileURL(path.join(root, 'missing.git')).toString() } }) + ) + ).toMatchObject({ outcome: 'failed', step: 'clone', reason: 'clone_failed' }); + }); + + it('stops a canceled build without running further setup', async () => { + const h = harness(); + const controller = new AbortController(); + const result = h.run( + buildInput({ commands: ['sleep 5', 'touch should-not-run'] }), + controller.signal + ); + setTimeout(() => controller.abort(), 200); + + expect(await result).toMatchObject({ outcome: 'failed', reason: 'canceled' }); + expect(await fs.stat(path.join(directory, 'should-not-run')).catch(() => null)).toBeNull(); + }); +}); + +describe('builder setup environment', () => { + it('drops the build credential and runtime names and keeps caches under the fixed home', () => { + const env = builderSetupEnvironment({ + inherited: { + PATH: '/bin', + ENVIRONMENT_BUILD_CREDENTIAL: 'jwt', + KILO_RUNTIME_CLI_PATH: '/opt/kilo', + HOME: '/root', + }, + vars: { FOO: 'bar', XDG_CACHE_HOME: '/elsewhere' }, + home: '/home/abc', + directory: '/workspace/app', + }); + + expect(env).toEqual({ + PATH: '/bin', + FOO: 'bar', + PWD: '/workspace/app', + HOME: '/home/abc', + XDG_DATA_HOME: '/home/abc/.local/share', + XDG_CONFIG_HOME: '/home/abc/.config', + XDG_CACHE_HOME: '/home/abc/.cache', + XDG_STATE_HOME: '/home/abc/.local/state', + GIT_TERMINAL_PROMPT: '0', + }); + }); + + it('renders setup events without repeating command text', () => { + expect( + setupEventLine({ + type: CONTROL_PLANE_SETUP_EVENTS.started, + properties: { command: 1, commandCount: 2 }, + }) + ).toBe('▶ Setup command 1 of 2'); + }); +}); + +describe('builder process stop', () => { + it('refuses to signal processes outside a builder container', async () => { + const refusal = await stopOtherProcesses({ pid: 4242, kill: () => undefined }).catch( + (error: unknown) => error + ); + expect(refusal).toBeInstanceOf(Error); + expect((refusal as Error).message).toContain('Refusing to stop processes'); + }); + + it('terminates then kills every other process when it is PID 1', async () => { + const signals: string[] = []; + await stopOtherProcesses({ + pid: 1, + kill: (target, signal) => signals.push(`${target}:${signal}`), + sleep: async () => undefined, + }); + expect(signals).toEqual(['-1:SIGTERM', '-1:SIGKILL']); + }); +}); + +describe('build reporter', () => { + it('delivers short-line output in valid batches without losing later lines', async () => { + const received: string[] = []; + const reporter = createBuildReporter({ + client: { + events: async batch => { + const parsed = environmentBuildEventsSchema.parse(batch); + received.push(...(parsed.logs ?? [])); + return { action: 'continue' }; + }, + }, + intervalMs: 60_000, + onCancel: () => undefined, + log: () => undefined, + }); + const lines = Array.from({ length: 650 }, (_, i) => `line ${i}`); + lines.forEach(line => reporter.line(line)); + await reporter.flush(); + expect(received).toEqual(lines.slice(0, 512)); + await reporter.flush(); + expect(received).toEqual(lines); + }); + + it('sends the step once, batches lines with a sequence and retains them after a failure', async () => { + const batches: EnvironmentBuildEvents[] = []; + let fail = true; + const reporter = createBuildReporter({ + client: { + events: async batch => { + if (fail) { + fail = false; + throw new Error('offline'); + } + batches.push(batch); + return { action: 'continue' }; + }, + }, + intervalMs: 60_000, + onCancel: () => undefined, + log: () => undefined, + }); + reporter.setStep('setup'); + reporter.line('one'); + await reporter.flush(); + reporter.line('two'); + await reporter.flush(); + await reporter.flush(); + + expect(batches).toEqual([ + { sequence: 1, step: 'setup', logs: ['one', 'two'] }, + { sequence: 2 }, + ]); + }); + + it('aborts the build on a cancel reply', async () => { + let canceled = false; + const reporter = createBuildReporter({ + client: { events: async () => ({ action: 'cancel' }) }, + intervalMs: 60_000, + onCancel: () => { + canceled = true; + }, + log: () => undefined, + }); + await reporter.flush(); + expect(canceled).toBe(true); + }); +}); diff --git a/services/cloud-agent-next/wrapper/src/environment-builder/run.ts b/services/cloud-agent-next/wrapper/src/environment-builder/run.ts new file mode 100644 index 00000000000..1d8d998c0a1 --- /dev/null +++ b/services/cloud-agent-next/wrapper/src/environment-builder/run.ts @@ -0,0 +1,207 @@ +import path from 'node:path'; +import { + CONTROL_RUNTIME_RESERVED_ENV_VARS, + RESERVED_ENV_VAR_PREFIXES, +} from '../../../src/shared/runtime-environment.js'; +import { + CONTROL_PLANE_SETUP_EVENTS, + type ControlPlaneSetupEvent, +} from '../../../src/shared/control-plane-protocol.js'; +import type { + EnvironmentBuildInput, + EnvironmentBuildResult, + EnvironmentBuildRunnerFailure, + EnvironmentBuildRunnerStep, +} from '../../../src/shared/environment-build-protocol.js'; +import { WrapperBootstrapError } from '../bootstrap-error.js'; +import { createSecretRedactor } from '../redact-output.js'; +import type { ExecResult, ProcessOptions } from '../utils.js'; +import type { RepositoryPreparation } from '../control-plane/repository-preparation.js'; +import type { WorkspaceStamp } from '../control-plane/workspace-stamp.js'; +import { directoryHome } from '../control-plane/workspace-home.js'; +import type { CleanupInput } from './cleanup.js'; +import type { BuildReporter } from './reporter.js'; + +/** + * One environment build inside its dedicated container (plan step 5 to 7 of + * "Execution order"): clone the default branch and pin its commit, run the + * environment's setup commands in the canonical workspace with the fixed home, + * stop setup descendants, remove credentials, stamp the workspace with this + * builder's allocation and report ready. The coordinator then captures. + */ + +/** + * The setup environment: the builder's inherited process environment without + * the build credential and runtime-owned names, then the environment's + * variables and secrets, then the fixed home and the XDG directories under it, + * as a session's setup sees them, so caches land in the same paths. + */ +export function builderSetupEnvironment(input: { + inherited: NodeJS.ProcessEnv; + vars: Record; + home: string; + directory: string; +}): Record { + const reserved = new Set(CONTROL_RUNTIME_RESERVED_ENV_VARS); + const runtimeOwned = (name: string): boolean => + reserved.has(name) || + name === 'HOME' || + name === 'PWD' || + name.startsWith('ENVIRONMENT_BUILD_') || + RESERVED_ENV_VAR_PREFIXES.some(prefix => name.startsWith(prefix)); + const env: Record = {}; + for (const [name, value] of Object.entries(input.inherited)) { + if (value !== undefined && !runtimeOwned(name)) env[name] = value; + } + for (const [name, value] of Object.entries(input.vars)) { + if (!runtimeOwned(name)) env[name] = value; + } + return { + ...env, + PWD: input.directory, + HOME: input.home, + XDG_DATA_HOME: path.join(input.home, '.local', 'share'), + XDG_CONFIG_HOME: path.join(input.home, '.config'), + XDG_CACHE_HOME: path.join(input.home, '.cache'), + XDG_STATE_HOME: path.join(input.home, '.local', 'state'), + GIT_TERMINAL_PROMPT: '0', + }; +} + +/** One readable line per setup event; command text is not repeated in the log. */ +export function setupEventLine(event: ControlPlaneSetupEvent): string | null { + const properties = event.properties as Record; + const command = typeof properties.command === 'number' ? properties.command : undefined; + switch (event.type) { + case CONTROL_PLANE_SETUP_EVENTS.started: + return `▶ Setup command ${command} of ${String(properties.commandCount)}`; + case CONTROL_PLANE_SETUP_EVENTS.output: + return typeof properties.output === 'string' ? properties.output.replace(/\n$/, '') : null; + case CONTROL_PLANE_SETUP_EVENTS.finished: + return properties.exitCode === 0 + ? `✓ Setup command ${command} finished` + : `✗ ${typeof properties.safeError === 'string' ? properties.safeError : `Setup command ${command} failed`} (exit ${String(properties.exitCode)})`; + default: + return null; + } +} + +export type BuildRunDeps = { + input: EnvironmentBuildInput; + reporter: Pick; + repository: Pick; + runGit: (args: string[], options?: ProcessOptions) => Promise; + stopProcesses: () => Promise; + cleanup: (input: CleanupInput) => Promise; + writeStamp: (directory: string, stamp: WorkspaceStamp) => Promise; + /** Aborted on a cancel reply or at the build deadline. */ + signal: AbortSignal; + inherited: NodeJS.ProcessEnv; + logPath: string | undefined; + homeRoot?: string; + now?: () => number; +}; + +function failure( + step: EnvironmentBuildRunnerStep, + reason: EnvironmentBuildRunnerFailure, + detail?: string +): EnvironmentBuildResult { + return { outcome: 'failed', step, reason, ...(detail === undefined ? {} : { detail }) }; +} + +function setupFailureReason(error: unknown): EnvironmentBuildRunnerFailure { + return error instanceof WrapperBootstrapError && error.subtype === 'setup_command_timeout' + ? 'setup_timeout' + : 'setup_failed'; +} + +/** Run one build. Never throws: every outcome is a result the coordinator records. */ +export async function runEnvironmentBuild(deps: BuildRunDeps): Promise { + const { input, reporter, repository, signal } = deps; + const now = deps.now ?? Date.now; + const directory = input.directory; + const home = directoryHome(directory, deps.homeRoot); + const env = builderSetupEnvironment({ + inherited: deps.inherited, + vars: input.env, + home, + directory, + }); + const redact = createSecretRedactor( + {}, + [ + Object.fromEntries(input.secretKeys.map(key => [key, input.env[key]])), + { GIT_TOKEN: input.git.token }, + ], + input.secretKeys + ); + const git = { ...input.git }; + const startedAt = now(); + + let step: EnvironmentBuildRunnerStep = 'clone'; + try { + reporter.setStep('clone'); + await repository.cloneRepository({ + git, + directory, + env, + redact, + signal, + onProgress: detail => reporter.line(detail ?? 'Cloning the default branch'), + }); + signal.throwIfAborted(); + const commit = await repository.headCommit(directory, env); + const branchResult = await deps.runGit(['rev-parse', '--abbrev-ref', 'HEAD'], { + cwd: directory, + env, + inheritEnv: false, + }); + const branch = branchResult.exitCode === 0 ? branchResult.stdout.trim() : ''; + if (!/^[0-9a-f]{40}$/.test(commit) || branch.length === 0 || branch === 'HEAD') { + return failure('clone', 'clone_failed', 'default branch could not be resolved'); + } + reporter.line(`Default branch ${branch} at ${commit}`); + + step = 'setup'; + reporter.setStep('setup'); + if (input.commands.length === 0) reporter.line('No setup commands'); + await repository.runSetupCommands({ + commands: input.commands, + directory, + env, + redact, + signal, + onEvent: event => { + const line = setupEventLine(event); + if (line !== null) reporter.line(line); + }, + logContext: `environmentId=${input.environmentId} buildId=${input.buildId}`, + }); + const setupDurationMs = Math.max(0, now() - startedAt); + + step = 'cleanup'; + reporter.setStep('cleanup'); + signal.throwIfAborted(); + await deps.stopProcesses(); + const cleanupFailure = await deps.cleanup({ + git: args => deps.runGit(args, { cwd: directory, env, inheritEnv: false }), + directory, + home, + bareUrl: input.git.url, + canaries: input.git.token === undefined ? [] : [input.git.token], + logPath: deps.logPath, + }); + if (cleanupFailure !== null) return failure('cleanup', 'cleanup_failed', cleanupFailure); + // The builder's own allocation: a session restored from this snapshot sees a + // foreign stamp and adopts the workspace instead of trusting it. + await deps.writeStamp(directory, { allocationId: input.allocationId, commit }); + reporter.line('Workspace prepared; waiting for the snapshot'); + return { outcome: 'ready', commit, branch, setupDurationMs }; + } catch (error) { + if (signal.aborted) return failure(step, 'canceled'); + if (step === 'clone') return failure('clone', 'clone_failed'); + if (step === 'setup') return failure('setup', setupFailureReason(error)); + return failure('cleanup', 'cleanup_failed', 'cleanup did not complete'); + } +} diff --git a/services/cloud-agent-next/wrapper/src/environment-builder/runner-file.test.ts b/services/cloud-agent-next/wrapper/src/environment-builder/runner-file.test.ts new file mode 100644 index 00000000000..29c8596ecee --- /dev/null +++ b/services/cloud-agent-next/wrapper/src/environment-builder/runner-file.test.ts @@ -0,0 +1,79 @@ +import { execFileSync } from 'node:child_process'; +import { + ENVIRONMENT_BUILD_RUNNER_DELIVERY_SCRIPT, + ENVIRONMENT_BUILD_RUNNER_DIR, +} from '../../../src/shared/environment-build-protocol.js'; +import { describe, expect, it } from 'bun:test'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { takeRunner } from './runner-file.js'; + +const RUNNER = { url: 'https://worker.example/environment-build/e/b', credential: 'c'.repeat(32) }; + +async function tempFile(): Promise { + const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'builder-runner-')); + return path.join(directory, 'runner.json'); +} + +describe('takeRunner', () => { + it('does not recreate a consumed credential when delivery is retried', async () => { + const file = await tempFile(); + const script = ENVIRONMENT_BUILD_RUNNER_DELIVERY_SCRIPT.replaceAll( + ENVIRONMENT_BUILD_RUNNER_DIR, + path.dirname(file) + ); + const deliver = () => + execFileSync('/bin/sh', ['-c', script], { + env: { PATH: process.env.PATH, KILO_BUILDER_RUNNER: JSON.stringify(RUNNER) }, + }); + deliver(); + expect(await takeRunner({ path: file })).toEqual(RUNNER); + deliver(); + expect(await fs.stat(file).catch(() => null)).toBeNull(); + expect(await fs.readFile(`${file}.consumed`, 'utf8')).toBe(''); + }); + + it('reads the runner once and deletes the file', async () => { + const file = await tempFile(); + await fs.writeFile(file, JSON.stringify(RUNNER)); + + expect(await takeRunner({ path: file })).toEqual(RUNNER); + expect(await fs.stat(file).catch(() => null)).toBeNull(); + }); + + it('waits for a file written after the start', async () => { + const file = await tempFile(); + let polls = 0; + const runner = await takeRunner({ + path: file, + sleep: async () => { + polls += 1; + if (polls === 3) await fs.writeFile(file, JSON.stringify(RUNNER)); + }, + }); + + expect(runner).toEqual(RUNNER); + expect(polls).toBe(3); + }); + + it('gives up after the wait and deletes an invalid file', async () => { + const missing = await tempFile(); + let clock = 0; + expect( + await takeRunner({ + path: missing, + waitMs: 1_000, + now: () => clock, + sleep: async ms => { + clock += ms; + }, + }) + ).toBeNull(); + + const invalid = await tempFile(); + await fs.writeFile(invalid, '{"url":"not a url"}'); + expect(await takeRunner({ path: invalid })).toBeNull(); + expect(await fs.stat(invalid).catch(() => null)).toBeNull(); + }); +}); diff --git a/services/cloud-agent-next/wrapper/src/environment-builder/runner-file.ts b/services/cloud-agent-next/wrapper/src/environment-builder/runner-file.ts new file mode 100644 index 00000000000..956e57b3b92 --- /dev/null +++ b/services/cloud-agent-next/wrapper/src/environment-builder/runner-file.ts @@ -0,0 +1,55 @@ +import fs from 'node:fs/promises'; +import { + ENVIRONMENT_BUILD_RUNNER_PATH, + environmentBuildRunnerSchema, + type EnvironmentBuildRunner, +} from '../../../src/shared/environment-build-protocol.js'; + +/** The container DO writes the file right after the start; this bounds the wait. */ +export const RUNNER_FILE_WAIT_MS = 2 * 60_000; +const RUNNER_FILE_POLL_MS = 200; + +export type RunnerFileDeps = { + path?: string; + readFile?: (path: string) => Promise; + remove?: (path: string) => Promise; + sleep?: (ms: number) => Promise; + now?: () => number; + waitMs?: number; +}; + +/** + * Wait for the runner file, read it once and delete it, so the endpoint and + * credential live only in this process's memory before setup runs. Returns + * null when no valid file arrives within the wait. + */ +export async function takeRunner( + deps: RunnerFileDeps = {} +): Promise { + const file = deps.path ?? ENVIRONMENT_BUILD_RUNNER_PATH; + const readFile = deps.readFile ?? (target => fs.readFile(target, 'utf8')); + const remove = deps.remove ?? (target => fs.rm(target, { force: true })); + const sleep = deps.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms))); + const now = deps.now ?? Date.now; + const deadline = now() + (deps.waitMs ?? RUNNER_FILE_WAIT_MS); + for (;;) { + let text: string; + try { + text = await readFile(file); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + if (now() >= deadline) return null; + await sleep(RUNNER_FILE_POLL_MS); + continue; + } + // Mark consumption before deleting: launch retries must not recreate the credential. + await fs.writeFile(`${file}.consumed`, '', { mode: 0o600 }); + await remove(file); + try { + const parsed = environmentBuildRunnerSchema.safeParse(JSON.parse(text)); + return parsed.success ? parsed.data : null; + } catch { + return null; + } + } +} diff --git a/services/kilo-mcp/catalog.json b/services/kilo-mcp/catalog.json index 1fc00a2e8c9..ee4a4b391cc 100644 --- a/services/kilo-mcp/catalog.json +++ b/services/kilo-mcp/catalog.json @@ -8022,6 +8022,164 @@ ], "searchBlob": "cloudAgentSettings.setRepositorySettings Save Cloud Agent sandbox settings for a specific repository, setting or clearing its assigned profile and allocation. cloudagentsettings setrepositorysettings organizationid repofullname platform profileid allocation organizationId repoFullName platform profileId allocation" }, + "cloudEnvironments.build": { + "path": "cloudEnvironments.build", + "kind": "mutation", + "summary": "Start or trigger a new build of a cloud environment at a specific revision, returning the request result. Use to launch a build for an environment.", + "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)$" + }, + "environmentId": { + "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)$" + }, + "expectedRevision": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "requestId": { + "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": [ + "environmentId", + "expectedRevision", + "requestId" + ] + }, + "tags": [ + "cloudenvironments", + "build", + "organizationid", + "environmentid", + "expectedrevision", + "requestid" + ], + "searchBlob": "cloudEnvironments.build Start or trigger a new build of a cloud environment at a specific revision, returning the request result. Use to launch a build for an environment. cloudenvironments build organizationid environmentid expectedrevision requestid organizationId environmentId expectedRevision requestId" + }, + "cloudEnvironments.buildLogs": { + "path": "cloudEnvironments.buildLogs", + "kind": "query", + "summary": "Fetch the log output of a specific cloud environment build, paginated by cursor, for viewing build output and debugging build failures.", + "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)$" + }, + "environmentId": { + "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)$" + }, + "buildId": { + "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)$" + }, + "cursor": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "environmentId", + "buildId" + ] + }, + "tags": [ + "cloudenvironments", + "buildlogs", + "organizationid", + "environmentid", + "buildid", + "cursor" + ], + "searchBlob": "cloudEnvironments.buildLogs Fetch the log output of a specific cloud environment build, paginated by cursor, for viewing build output and debugging build failures. cloudenvironments buildlogs organizationid environmentid buildid cursor organizationId environmentId buildId cursor" + }, + "cloudEnvironments.buildStatus": { + "path": "cloudEnvironments.buildStatus", + "kind": "query", + "summary": "Check the current status of a cloud environment's build, such as whether it is queued, running, succeeded, or failed. Use to poll build progress for an environment.", + "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)$" + }, + "environmentId": { + "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": [ + "environmentId" + ] + }, + "tags": [ + "cloudenvironments", + "buildstatus", + "organizationid", + "environmentid" + ], + "searchBlob": "cloudEnvironments.buildStatus Check the current status of a cloud environment's build, such as whether it is queued, running, succeeded, or failed. Use to poll build progress for an environment. cloudenvironments buildstatus organizationid environmentid organizationId environmentId" + }, + "cloudEnvironments.cancelBuild": { + "path": "cloudEnvironments.cancelBuild", + "kind": "mutation", + "summary": "Cancel a running or queued build of a cloud environment by build ID. Use to stop or abort an in-progress environment build.", + "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)$" + }, + "environmentId": { + "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)$" + }, + "buildId": { + "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": [ + "environmentId", + "buildId" + ] + }, + "tags": [ + "cloudenvironments", + "cancelbuild", + "organizationid", + "environmentid", + "buildid" + ], + "searchBlob": "cloudEnvironments.cancelBuild Cancel a running or queued build of a cloud environment by build ID. Use to stop or abort an in-progress environment build. cloudenvironments cancelbuild organizationid environmentid buildid organizationId environmentId buildId" + }, "cloudEnvironments.create": { "path": "cloudEnvironments.create", "kind": "mutation",